Mercurial > hg > ccmieditor
changeset 0:9418ab7b7f3f
Initial import
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/.classpath Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/> + <classpathentry kind="lib" path="libs/beads.jar"/> + <classpathentry kind="lib" path="libs/jl1.0.1.jar"/> + <classpathentry kind="lib" path="libs/JWizardComponent.jar"/> + <classpathentry kind="lib" path="libs/mp3spi1.9.4.jar"/> + <classpathentry kind="lib" path="libs/NetUtil.jar"/> + <classpathentry kind="lib" path="libs/tritonus_share.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/.project Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ccmieditor</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/.settings/org.eclipse.jdt.core.prefs Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,12 @@ +#Fri Dec 16 13:28:00 GMT 2011 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionEvent.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,52 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.EventObject; + +/** + * An object representing a collection event. Collection events happen when a Diagram + * Element is either inserted or removed from the DiagramModel via the CollectionModel. + * + */ +@SuppressWarnings("serial") +public class CollectionEvent extends EventObject { + + /** + * + * @param source the source of the event + * @param element the diagram element that has been added or removed from the collection + */ + public CollectionEvent(Object source, DiagramElement element) { + super(source); + this.element = element; + } + + /** + * + * @return the diagram element whose addition or removal from the collection + * triggered this event. + */ + public DiagramElement getDiagramElement(){ + return element; + } + + private DiagramElement element; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionListener.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,34 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * A listener to a collection event. A collection event is triggered whenever a diagram element + * is added or removed from the collection or when a diagram element contained in the collection is + * changed (e.g. when a new Name for the element is set) + * + */ +public interface CollectionListener { + + void elementInserted(CollectionEvent e); + + void elementTakenOut(CollectionEvent e); + + void elementChanged(ElementChangedEvent e); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionModel.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,126 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.Collection; + +import javax.swing.event.ChangeListener; + +/** + * + * Represents the collection side of a DiagramModel instance. + * + * @param <N> a type extending DiagramNode + * @param <E> a type extending DiagramEdge + */ +public interface CollectionModel<N extends DiagramNode, E extends DiagramEdge> { + /** + * Adds a collection listener to the model. + * @param listener the listener to be added + */ + void addCollectionListener(CollectionListener listener); + /** + * Removed a collection listener to the model. + * @param listener the listener to be removed + */ + void removeCollectionListener (CollectionListener listener); + + /** + * insert a DiagramNode into the diagram model + * @param n the DiagramNode to be inserted in the collection + * @return true if this collection changed as a result of the call + */ + boolean insert(N n) ; + + /** + * insert a DiagramEdge into the diagram model + * @param e the DiagramNode to be inserted in the collection + * @return true if this collection changed as a result of the call + */ + boolean insert(E e); + + /** + * Removes a DiagramElement from the model + * @param e the diagramElement to be removed + * @return true if this collection changed as a result of the call + */ + boolean takeOut(DiagramElement e); + + /** + * Returns the diagram nodes contained by the model as a Collection + * @return the collection of diagram nodes + */ + Collection<N> getNodes(); + + /** + * Returns the diagram edges contained by the model as a Collection + * @return the collection of diagram edges + */ + Collection<E> getEdges(); + + /** + * return a list of nodes and edges in the model as a unique collection + * of Diagram elements. + * @return + */ + Collection<DiagramElement> getElements(); + + /** + * Add a change listener to the model. the listeners will be fired each time the model + * goes from the unmodified to modified state. The model is modified when a either a + * node or an edge are inserted or removed or changed when they are within the model. + * @param l a ChangeListener to add to the model + */ + void addChangeListener(ChangeListener l); + + /** + * Removes a change listener from the model. + * @param l a ChangeListener to remove from the model + */ + void removeChangeListener(ChangeListener l); + + /** + * Returns true if the model has been modified + * @return true if the model has been modified + */ + boolean isModified(); + + /** + * Sets the model as unmodified. This entails that {@link #isModified()} will return + * false unless the model doesn't get modified again. After this call a new modification + * of the model would trigger the associated change listeners again. + */ + void setUnmodified(); + + /** + * Sorts the nodes and edges is the model. The ordering method is given by a diagram + * element comparator. + * @see DiagramElementComparator + */ + void sort(); + + /** + * Returns an object that can be used to access the nodes and edges (via {@link #getNodes()} + * and {@link #getEdges()} in a synchronized block. The monitor is guaranteed to give + * exclusive access even in regards to the access via the tree side of the model. + * @return + */ + Object getMonitor(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ConnectNodesException.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,33 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + + +/** + * Represents the exception that is raised when a number of nodes are + * connected through an edge which allows for a different number of nodes only. + * + * @see DiagramEdge#connect(java.util.List) + */ +@SuppressWarnings("serial") +public class ConnectNodesException extends Exception { + public ConnectNodesException(String message){ + super(message); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramEdge.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,169 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * This class represent an edge in the diagram model. Note that this class is + * a tree node. + * + */ +@SuppressWarnings("serial") +public abstract class DiagramEdge extends DiagramElement { + /** + * + * @param type the type of the edge + * @param availableEndDescriptions the end descriptions this edge allows + */ + public DiagramEdge(String type, String[] availableEndDescriptions){ + setType(type); + this.availableEndDescriptions = availableEndDescriptions; + endLabels = new LinkedHashMap<DiagramNode,String>(); + endDescriptions = new LinkedHashMap<DiagramNode,Integer>(); + } + + /** + * Returns a more detailed description of the edge than {@link #spokenText()}. + * the description includes which nodes this edge is connecting + * + * @return a description of the edge + */ + @Override + public String detailedSpokenText(){ + final String and = " and "; + StringBuilder b = new StringBuilder(getType()); + b.append(' ').append(spokenText()); + b.append(". Connecting "); + for(int i=0; i<getChildCount();i++){ + b.append(((DiagramModelTreeNode)getChildAt(i)).getName()); + b.append(and); + } + // remove the last " and " + b.delete(b.length()-and.length(), b.length()); + return b.toString(); + } + + /** + * Set a label related to a node. On a graphical representation of the diagram + * the label would be put in proximity of the node. + * + * @param n the node,at whose end the label is located + * @param label the label + */ + public void setEndLabel(DiagramNode n, String label){ + if(label == null) + label = ""; + endLabels.put(n, label); + notifyChange(new ElementChangedEvent(n,this,"endLabel")); + } + + /** + * Returns the end label related to a node. On a graphical representation of the diagram + * the label would be put in proximity of the node. + * + * @param n the node,at whose end the label is located + * @param label the label + */ + public String getEndLabel(DiagramNode n){ + String s = endLabels.get(n); + if(s == null) + return ""; + return s; + } + + /** + * Returns an array with all the available end description this edge can be + * assigned. + * @return an array of string available end description + */ + public String[] getAvailableEndDescriptions(){ + return this.availableEndDescriptions; + } + + /** + * Set a string describing the end related to a node. On a visual diagram this + * corresponds to an arrow. + * + * @param n the node at the edge end whose description will be set + * @param index an index of the array returned by getAvailableEndDescriptions(). The + * edge end description will be set with the string at that position in the array. + * if index is equal to NO_END_DESCRIPTION_INDEX, then the description will be set + * as the empty string. + * + */ + public void setEndDescription(DiagramNode n, int index){ + endDescriptions.put(n, index); + notifyChange(new ElementChangedEvent(n,this,"arrowHead")); + } + + /** + * Returns a string describing the end related to a node. On a visual diagram this + * corresponds to an arrow. + * + * @param n the node at the edge end whose description will be returned + * @return a description string + */ + public String getEndDescription(DiagramNode n){ + Integer index = endDescriptions.get(n); + if(index == null || index == NO_END_DESCRIPTION_INDEX) + return ""; + return availableEndDescriptions[endDescriptions.get(n)]; + } + + /** + * Returns the connected node at the specified index + * @param index an index into node's list + * @return the connected node at the specified index + */ + public abstract DiagramNode getNodeAt(int index); + + /** + * Returns the number of nodes this edge is connecting + * @return the number of nodes this edge is connecting + */ + public abstract int getNodesNum(); + + /** + * Removes a node from this edge. On a graphical representation of the diagram + * this would mean that the node is no longer connected to the other nodes via this edge. + * @param n the node to be removed + * @return true if the inner collection changed as a result of the call + */ + public abstract boolean removeNode(DiagramNode n); + + /** + * Connect a list of nodes with this edge + * @param nodes a list of nodes to connect + * @throws ConnectNodesException if the number of nodes in the list is different + * from the number allowed by this edge. + */ + public abstract void connect(List<DiagramNode> nodes) throws ConnectNodesException; + + /** + * An index to be passed to {@link #setEndDescription(DiagramNode, int)} in order + * to set the edge with no description at the end related to that diagram node + */ + public static int NO_END_DESCRIPTION_INDEX = -1; + private Map<DiagramNode,String> endLabels; + private Map<DiagramNode,Integer> endDescriptions; + private String[] availableEndDescriptions; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramElement.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,173 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.io.InputStream; + +/** + * A Diagram Element is either a node or an edge of the diagram. It's an abstract + * class which is extended by DiagramEdge and DiagramNode. + * + */ +@SuppressWarnings("serial") +public abstract class DiagramElement extends DiagramModelTreeNode implements Cloneable{ + + protected DiagramElement(){ + name = ""; + id = NO_ID; + notifier = DUMMY_NOTIFIER; // initially set to no effect notifier + } + + /** + * Returns the type of this diagram element. The type is the same for all the elements + * which fall under the same type, whereas a different name can be assigned to each + * instance of such elements. + * @return the type of this element + */ + public String getType(){ + return type; + } + + /** + * Set the type of this diagram element. This method should be called as soon as the object is created + * and should not be called anymore on this object. + * @param type the type of this element + */ + protected void setType(String type){ + this.type = type; + } + + /** + * Notifies the model of a changed that has happened on this element. If this element is not + * held by any model than this method will have no effect. + * @param evt an event representing the fact that the element is changed + */ + protected void notifyChange(ElementChangedEvent evt){ + notifier.notifyChange(evt); + } + + /** + * returns the tree node name "as it is", without any decoration such as notes, bookmarks or cardinality. + * Unlike the String returned by toString + * @return the tree node name + */ + public String getName(){ + if(name.isEmpty() && id != NO_ID) + return "new " + getType() + " " + id; + return name; + } + + /** + * Sets the name of this element instance. + * + * @param the string to set as the name + */ + public void setName(String s){ + String name = s; + /* if the user enters an empty string we go back to the default name */ + if(s.isEmpty() && id != NO_ID){ + name = "new " + getType() + " " + id; + } + setUserObject(name); + this.name = name; + notifyChange(new ElementChangedEvent(this,this,"name")); + } + + /** + * Returns an InputStream to a sound file with the sound of this element + * @return an InputStream + */ + public abstract InputStream getSound(); + + /** + * Sets the if for this element. The id is a number which uniquely identifies this instance + * within a DiagramModel. + * Unlike the name, which can be the same for two different instances. + * @param id a long number which must be greater than 0 + * @throws IllegalArgumentException id the id passe as argument is lower or equal to 0. + */ + public void setId(long id){ + if (id < NO_ID) + throw new IllegalArgumentException(); + else + this.id = id; + if(name.isEmpty() && id != NO_ID){ + String s = "new " + getType() + " " + id; + this.name = s; + setUserObject(s); + } + } + + /** + * Returns the id of this instance of DiagramElement. + * @return a long representing the id of this instance of the element + * or NO_ID if it hasn't got one. + */ + public long getId(){ + return id; + } + + /** + * Sets the notifier to be used for notification + * following an internal change of the node + * @param notifier the notifier call the notify method(s) on + */ + public void setNotifier(ElementNotifier notifier){ + this.notifier = notifier; + } + + @Override + public Object clone(){ + DiagramElement clone = (DiagramElement)super.clone(); + clone.name = ""; + clone.id = NO_ID; + return clone; + } + + /** + * Returns a description of the DiagramElement passed as argument, suitable + * for logging purposes. + * + * @param de the diagram element to log stuff about + * @return a log entry describing the element passed as agument + */ + public static String toLogString(DiagramElement de){ + StringBuilder builder = new StringBuilder(de.getName()); + builder.append('('); + if(de.getId() == DiagramElement.NO_ID) + builder.append("no id"); + else + builder.append(de.getId()); + builder.append(')'); + return builder.toString(); + } + + private long id = NO_ID; + private ElementNotifier notifier; + private String type; + private String name; + private static final ElementNotifier DUMMY_NOTIFIER = new ElementNotifier(){ + @Override + public void notifyChange(ElementChangedEvent evt) {} + }; + /** + * The value returned by getId() if the element instance has not been assigned any id + */ + public static final long NO_ID = 0; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramElementComparator.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,46 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.Comparator; + +/** + * A Comparator for diagram elements. The elements are ordered by their id. + * + * @see DiagramElement#getId() + */ +public class DiagramElementComparator implements Comparator<DiagramElement> { + public static DiagramElementComparator getInstance(){ + if(comparator == null) + comparator = new DiagramElementComparator(); + return comparator; + } + + @Override + public int compare(DiagramElement de1, DiagramElement de2) { + if(de1.getId() == de2.getId()) + return 0; + else if(de1.getId() < de2.getId()) + return -1; + else + return 1; + } + + private static DiagramElementComparator comparator; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModel.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,781 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeNode; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; +import uk.ac.qmul.eecs.ccmi.utils.Pair; + +/** + * This class represent a model as per in the model-view control architecture. + * The model is "double sided" in the sense that it can be accessed through either + * a CollectionModel or a TreeModel returned by the respective getter methods. + * The TreeModel is suitable for JTree classes of the swing library, while + * the CollectionModel can be used by view classes by registering a CollectionListener + * to the CollectionModel itself. + * It is important to notice that changes made on one side will reflect on the other, + * eventually triggering the registered listeners. + * The tree model is structured according to a special layout which is suitable for + * browsing the tree view via audio interface ( text to speech synthesis and sound). + * + * @param <N> a subclass of DiagramNode + * @param <E> a subclass of DiagramEdge + */ +public class DiagramModel<N extends DiagramNode, E extends DiagramEdge>{ + /** + * Create a model instance starting from some nodes and edges prototypes. + * All subsequently added element must be clones of such prototypes. + * @param nodePrototypes + * @param edgePrototypes + */ + @SuppressWarnings("serial") + public DiagramModel(N [] nodePrototypes, E [] edgePrototypes) { + root = new DiagramModelTreeNode(ROOT_LABEL){ + @Override + public boolean isRoot(){ + return true; + } + }; + modified = false; + + nodeCounter = 0; + edgeCounter = 0; + + treeModel = new InnerTreeModel(root); + treeModel.setEventSource(treeModel);/* default event source is the tree itself */ + diagramCollection = new InnerDiagramCollection(); + + nodes = new ArrayList<N>(INITIAL_NODES_SIZE); + edges = new ArrayList<E>(INITIAL_EDGES_SIZE); + elements = new ArrayList<DiagramElement>(INITIAL_NODES_SIZE+INITIAL_EDGES_SIZE); + + changeListeners = new LinkedList<ChangeListener>(); + + notifier = new ElementNotifier(){ + @Override + public void notifyChange(ElementChangedEvent evt) { + _change(evt); + triggerModification(evt.getDiagramElement()); + } + }; + + for(N n : nodePrototypes) + addType(n); + for(E e : edgePrototypes){ + addType(e); + } + } + + /** + * Returns a CollectionModel for this diagram + * + * @return a CollectionModel for this diagram + */ + public CollectionModel<N,E> getDiagramCollection(){ + return diagramCollection; + } + + /** + * Returns a TreeModel for this diagram + * + * @return a TreeModel for this diagram + */ + public TreeModel<N,E> getTreeModel(){ + return treeModel; + } + + private void triggerModification(Object source){ + if(modified) // fire the listener only the first time a change happens + return; + modified = true; + fireChangeListeners(source); + } + + private void addChangeListener(ChangeListener l){ + changeListeners.add(l); + } + + private void removeChangeListener(ChangeListener l){ + changeListeners.remove(l); + } + + protected void fireChangeListeners(Object source){ + ChangeEvent changeEvent = new ChangeEvent(source); + for(ChangeListener l : changeListeners) + l.stateChanged(changeEvent); + } + + private void addType(DiagramElement element){ + DiagramModelTreeNode typeNode = _lookForChild(root, element.getType()); + if(typeNode == null){ + typeNode = new TypeMutableTreeNode(element); + treeModel.insertNodeInto(typeNode, root, root.getChildCount()); + } + } + + private class InnerDiagramCollection implements CollectionModel<N,E> { + + public InnerDiagramCollection(){ + listeners = new ArrayList<CollectionListener>(); + } + + @Override + public boolean insert(N n){ + return _insert(n,this); + } + + @Override + public boolean insert(E e){ + return _insert(e,this); + } + + @Override + public boolean takeOut(DiagramElement element){ + if(element instanceof DiagramNode) + return _takeOut((DiagramNode)element,this); + if(element instanceof DiagramEdge) + return _takeOut((DiagramEdge)element,this); + return false; + } + + @Override + public void addCollectionListener(CollectionListener listener) { + listeners.add(listener); + } + + @Override + public void removeCollectionListener(CollectionListener listener) { + listeners.remove(listener); + } + + protected void fireElementInserted(Object source, DiagramElement element) { + for(CollectionListener l : listeners){ + l.elementInserted(new CollectionEvent(source,element)); + } + } + + protected void fireElementTakenOut(Object source, DiagramElement element) { + for(CollectionListener l : listeners){ + l.elementTakenOut(new CollectionEvent(source,element)); + } + } + + protected void fireElementChanged(ElementChangedEvent evt){ + for(CollectionListener l : listeners){ + l.elementChanged(evt); + } + } + + @Override + public Collection<N> getNodes() { + return Collections.unmodifiableCollection(nodes); + } + + @Override + public Collection<E> getEdges() { + return Collections.unmodifiableCollection(edges); + } + + @Override + public Collection<DiagramElement> getElements(){ + return Collections.unmodifiableCollection(elements); + } + + @Override + public Object getMonitor(){ + return DiagramModel.this; + } + + @Override + public void addChangeListener(ChangeListener l){ + DiagramModel.this.addChangeListener(l); + } + + @Override + public void removeChangeListener(ChangeListener l){ + DiagramModel.this.removeChangeListener(l); + } + + /* sort the collections according to the id of nodes */ + public void sort(){ + Collections.sort(nodes, DiagramElementComparator.getInstance()); + Collections.sort(edges, DiagramElementComparator.getInstance()); + } + + public boolean isModified(){ + return modified; + } + + public void setUnmodified(){ + modified = false; + } + + protected ArrayList<CollectionListener> listeners; + + } + + @SuppressWarnings("serial") + private class InnerTreeModel extends DefaultTreeModel implements TreeModel<N,E>{ + + public InnerTreeModel(TreeNode root){ + super(root); + bookmarks = new LinkedHashMap<String, DiagramModelTreeNode>(); + } + + @Override + public boolean insertTreeNode(N treeNode){ + return _insert(treeNode,this); + } + + @Override + public boolean insertTreeNode(E treeNode){ + return _insert(treeNode,this); + } + + @Override + public boolean takeTreeNodeOut(DiagramElement treeNode){ + boolean result; + if(treeNode instanceof DiagramEdge){ + result = _takeOut((DiagramEdge)treeNode,this); + } + else{ + result = _takeOut((DiagramNode)treeNode,this); + } + /* remove the bookmarks associated with the just deleted diagram element, if any */ + for(String key : treeNode.getBookmarkKeys()) + bookmarks.remove(key); + return result; + } + + @Override + public DiagramModelTreeNode putBookmark(String bookmark, DiagramModelTreeNode treeNode){ + if(bookmark == null) + throw new IllegalArgumentException("bookmark cannot be null"); + setEventSource(this); + treeNode.addBookmarkKey(bookmark); + DiagramModelTreeNode result = bookmarks.put(bookmark, treeNode); + nodeChanged(treeNode); + iLog("bookmark added",bookmark); + triggerModification(this); + return result; + } + + @Override + public DiagramModelTreeNode getBookmarkedTreeNode(String bookmark) { + return bookmarks.get(bookmark); + } + + @Override + public DiagramModelTreeNode removeBookmark(String bookmark) { + setEventSource(this); + DiagramModelTreeNode treeNode = bookmarks.remove(bookmark); + treeNode.removeBookmarkKey(bookmark); + nodeChanged(treeNode); + iLog("bookmark removed",bookmark); + triggerModification(this); + return treeNode; + } + + @Override + public Set<String> getBookmarks(){ + return new LinkedHashSet<String>(bookmarks.keySet()); + } + + @Override + public void setNotes(DiagramModelTreeNode treeNode, String notes){ + setEventSource(this); + treeNode.setNotes(notes); + nodeChanged(treeNode); + iLog("notes set for "+treeNode.getName(),"".equals(notes) ? "empty notes" : notes.replaceAll("\n", "\\\\n")); + triggerModification(this); + } + + private void setEventSource(Object source){ + this.src = source; + } + + @Override + public Object getMonitor(){ + return DiagramModel.this; + } + + @Override + public void addChangeListener(ChangeListener l){ + DiagramModel.this.addChangeListener(l); + } + + @Override + public void removeChangeListener(ChangeListener l){ + DiagramModel.this.removeChangeListener(l); + } + + /* redefine the fire methods so that they set the source object according */ + /* to whether the element was inserted from the graph or from the tree */ + @Override + protected void fireTreeNodesChanged(Object source, Object[] path, + int[] childIndices, Object[] children) { + super.fireTreeNodesChanged(src, path, childIndices, children); + } + + @Override + protected void fireTreeNodesInserted(Object source, Object[] path, + int[] childIndices, Object[] children) { + super.fireTreeNodesInserted(src, path, childIndices, children); + } + + @Override + protected void fireTreeNodesRemoved(Object source, Object[] path, + int[] childIndices, Object[] children) { + super.fireTreeNodesRemoved(src, path, childIndices, children); + } + + @Override + protected void fireTreeStructureChanged(Object source, Object[] path, + int[] childIndices, Object[] children) { + super.fireTreeStructureChanged(src, path, childIndices, children); + } + + public boolean isModified(){ + return modified; + } + + public void setUnmodified(){ + modified = false; + } + + private Object src; + private Map<String, DiagramModelTreeNode> bookmarks; + } + + private synchronized boolean _insert(N n, Object source) { + assert(n != null); + + /* if id has already been given then sync the counter so that a surely new value is given to the next nodes */ + if(n.getId() == DiagramElement.NO_ID) + n.setId(++nodeCounter); + else if(n.getId() > nodeCounter) + nodeCounter = n.getId(); + + treeModel.setEventSource(source); + nodes.add(n); + elements.add(n); + /* add the node to outer node's (if any) inner nodes */ + if(n.getExternalNode() != null) + n.getExternalNode().addInternalNode(n); + + /* decide where to insert the node based on whether this is an inner node or not */ + MutableTreeNode parent; + if(n.getExternalNode() == null){ + DiagramModelTreeNode typeNode = _lookForChild(root, n.getType()); + if(typeNode == null) + throw new IllegalArgumentException("Node type "+n.getType()+" not present in the model"); + parent = typeNode; + }else{ + parent = n.getExternalNode(); + } + + /* add to the node one child per property type */ + for(String propertyType : n.getProperties().getTypes()) + n.insert(new PropertyTypeMutableTreeNode(propertyType,n), n.getChildCount()); + + /* inject the notifier for managing changes internal to the edge */ + n.setNotifier(notifier); + + /* insert node into tree which fires tree listeners */ + treeModel.insertNodeInto(n, parent, parent.getChildCount()); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(parent); + diagramCollection.fireElementInserted(source,n); + triggerModification(n); + + iLog("node inserted",DiagramElement.toLogString(n)); + return true; + } + + private synchronized boolean _takeOut(DiagramNode n, Object source) { + treeModel.setEventSource(source); + /* recursively remove internal nodes of this node */ + _removeInternalNodes(n,source); + /* clear external node and clear edges attached to this node and updates other ends of such edges */ + _clearNodeReferences(n,source); + /* remove the node from the tree (fires listeners) */ + treeModel.removeNodeFromParent(n); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(n.getParent()); + /* remove the nodes from the collection */ + nodes.remove(n); + elements.remove(n); + /* notify all the listeners a new node has been removed */ + diagramCollection.fireElementTakenOut(source,n); + triggerModification(n); + + if(nodes.isEmpty()){ + nodeCounter = 0; + }else{ + long lastNodeId = nodes.get(nodes.size()-1).getId(); + if(n.getId() > lastNodeId) + nodeCounter = lastNodeId; + } + iLog("node removed",DiagramElement.toLogString(n)); + return true; + } + + private synchronized boolean _insert(E e, Object source) { + assert(e != null); + /* executes formal controls over the edge's node, which must be specified from the outer class*/ + if(e.getNodesNum() < 2) + throw new MalformedEdgeException(e,"too few (" +e.getNodesNum()+ ") nodes"); + + /* if id has already been given then sync the counter so that a surely new value is given to the next edges */ + if(e.getId() > edgeCounter) + edgeCounter = e.getId(); + else + e.setId(++edgeCounter); + + treeModel.setEventSource(source); + edges.add(e); + elements.add(e); + + /* updates the nodes' edge reference and the edge tree references */ + for(int i = e.getNodesNum()-1; i >= 0; i--){ + DiagramNode n = e.getNodeAt(i); + assert(n != null); + /* insert first the type of the edge, if not already present */ + DiagramModelTreeNode edgeType = _lookForChild(n, e.getType()); + if(edgeType == null){ + edgeType = new EdgeReferenceHolderMutableTreeNode(e.getType()); + treeModel.insertNodeInto(edgeType, n, 0); + } + + /* insert the edge reference under its type tree node, in the Node*/ + treeModel.insertNodeInto(new EdgeReferenceMutableTreeNode(e,n), edgeType, 0); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(edgeType); + + n.addEdge(e); + /* insert the node reference into the edge tree node */ + e.insert(new NodeReferenceMutableTreeNode(n,e), 0); + } + + DiagramModelTreeNode parent = _lookForChild(root, e.getType()); + if(parent == null) + throw new IllegalArgumentException("Edge type "+e.getType()+" not present in the model"); + + /* inject the controller and notifier to manage changes internal to the edge */ + e.setNotifier(notifier); + + /* c'mon baby light my fire */ + treeModel.insertNodeInto(e, parent, parent.getChildCount()); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(parent); + diagramCollection.fireElementInserted(source,e); + triggerModification(e); + + StringBuilder builder = new StringBuilder(DiagramElement.toLogString(e)); + builder.append(" connecting:"); + for(int i=0; i<e.getNodesNum();i++) + builder.append(DiagramElement.toLogString(e.getNodeAt(i))).append(' '); + + iLog("edge inserted",builder.toString()); + return true; + } + + private synchronized boolean _takeOut(DiagramEdge e, Object source) { + treeModel.setEventSource(source); + /* update the nodes attached to this edge */ + _clearEdgeReferences(e); + /* remove the edge from the collection */ + edges.remove(e); + elements.remove(e); + /* remove the edge from the tree (fires tree listeners) */ + treeModel.removeNodeFromParent(e); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(e.getParent()); + /* notify listeners for collection */ + diagramCollection.fireElementTakenOut(source,e); + triggerModification(e); + + if(edges.isEmpty()){ + edgeCounter = 0; + }else{ + long lastEdgeId = edges.get(edges.size()-1).getId(); + if(e.getId() > lastEdgeId) + edgeCounter = lastEdgeId; + } + iLog("edge removed",DiagramElement.toLogString(e)); + return true; + } + + private void _removeInternalNodes(DiagramNode n, Object source){ + for(int i=0; i<n.getInternalNodesNum(); i++){ + DiagramNode innerNode = n.getInternalNodeAt(i); + _clearNodeReferences(innerNode, source); + _removeInternalNodes(innerNode, source); + n.removeInternalNode(innerNode); + nodes.remove(n); + } + } + + /* removes both inner and tree node references to an edge from nodes it's attached to */ + private void _clearEdgeReferences(DiagramEdge e){ + for(int i=0; i<e.getNodesNum();i++){ + DiagramNode n = e.getNodeAt(i); + EdgeReferenceMutableTreeNode reference; + /* find the category tree node under which our reference is */ + + reference = _lookForEdgeReference(n, e); + assert(reference != null); + + treeModel.removeNodeFromParent(reference); + DiagramModelTreeNode type = _lookForChild(n, e.getType()); + if(type.isLeaf()) + treeModel.removeNodeFromParent(type); + n.removeEdge(e); + } + } + + /* removes references from node */ + private void _clearNodeReferences(DiagramNode n, Object source){ + /* remove the node itself from its external node, if any */ + if(n.getExternalNode() != null) + n.getExternalNode().removeInternalNode(n); + /* remove edges attached to this node from the collection */ + ArrayList<DiagramEdge> edgesToRemove = new ArrayList<DiagramEdge>(edges.size()); + for(int i=0; i<n.getEdgesNum(); i++){ + DiagramEdge e = n.getEdgeAt(i); + if(e.getNodesNum() == 2){ + edgesToRemove.add(e); + }else{ + e.removeNode(n); + DiagramModelTreeNode nodeTreeReference = _lookForNodeReference(e, n); + treeModel.removeNodeFromParent(nodeTreeReference); + } + + } + /* remove the edges that must no longer exist as were two ended edges attached to this node */ + for(DiagramEdge e : edgesToRemove) + _takeOut(e, source); + } + + private void _change(ElementChangedEvent evt){ + String changeType = evt.getChangeType(); + /* don't use the event source as it might collide with other threads as + * changes on the collections and inner changes on the node are synch'ed thought different monitors */ + /* treeModel.setEventSource(evt.getSource());*/ + if("name".equals(changeType)){ + if(evt.getSource() instanceof DiagramNode){ + DiagramNode n = (DiagramNode)evt.getSource(); + for(int i=0; i<n.getEdgesNum(); i++){ + DiagramEdge e = n.getEdgeAt(i); + treeModel.nodeChanged(_lookForNodeReference(e,n)); + treeModel.nodeChanged(_lookForEdgeReference(n,e)); + for(int j=0; j<e.getNodesNum(); j++){ + DiagramNode n2 = e.getNodeAt(j); + if(n2 != n) + treeModel.nodeChanged(_lookForEdgeReference(n2,e)); + } + } + iLog("node name changed",DiagramElement.toLogString(n)); + }else{ + DiagramEdge e = (DiagramEdge)evt.getSource(); + for(int i=0; i<e.getNodesNum();i++){ + DiagramNode n = e.getNodeAt(i); + treeModel.nodeChanged(_lookForEdgeReference(n,e)); + } + iLog("edge name changed",DiagramElement.toLogString(e)); + } + treeModel.nodeChanged(evt.getDiagramElement()); + }else if("properties".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + for(String type : n.getProperties().getTypes()){ + PropertyTypeMutableTreeNode typeNode = null; + for(int i=0; i<n.getChildCount();i++){ + /* find the child treeNode corresponding to the current type */ + if(n.getChildAt(i) instanceof PropertyTypeMutableTreeNode) + if(type.equals(((PropertyTypeMutableTreeNode)n.getChildAt(i)).getType())){ + typeNode = (PropertyTypeMutableTreeNode)n.getChildAt(i); + break; + } + } + + if(typeNode == null) + throw new IllegalArgumentException("Inserted Node property type "+ type + " not present in the tree" ); + + /* set the name and modifier string of all the children PropertyNodes */ + typeNode.setValues(n.getProperties().getValues(type), n.getProperties().getModifiers(type)); + } + treeModel.nodeStructureChanged(evt.getDiagramElement()); + iLog("node properties changed",n.getProperties().toString()); + }else if("properties.clear".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + List<String> empty = Collections.emptyList(); + for(int i=0; i<n.getChildCount();i++){ + /* find the child treeNode corresponding to the current type */ + if(n.getChildAt(i) instanceof PropertyTypeMutableTreeNode){ + ((PropertyTypeMutableTreeNode)n.getChildAt(i)).setValues(empty, null); + } + } + treeModel.nodeStructureChanged(evt.getDiagramElement()); + }else if("property.add".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + @SuppressWarnings("unchecked") + Pair<String,Integer> p = (Pair<String,Integer>)evt.getSource(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,p.first); + PropertyMutableTreeNode propertyNode = new PropertyMutableTreeNode(n.getProperties().getValues(p.first).get(p.second)); + typeNode.add(propertyNode); + treeModel.insertNodeInto(propertyNode, typeNode, p.second); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(typeNode); + iLog("property inserted",propertyNode.getName()); + }else if("property.set".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + @SuppressWarnings("unchecked") + Pair<String,Integer> p = (Pair<String,Integer>)evt.getSource(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,p.first); + ((DiagramModelTreeNode)typeNode.getChildAt(p.second)).setUserObject(n.getProperties().getValues(p.first).get(p.second)); + treeModel.nodeChanged((typeNode.getChildAt(p.second))); + iLog("property changed",n.getProperties().getValues(p.first).get(p.second)); + }else if("property.remove".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + @SuppressWarnings("unchecked") + Pair<String,Integer> p = (Pair<String,Integer>)evt.getSource(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,p.first); + iLog("property removed",((DiagramModelTreeNode)typeNode.getChildAt(p.second)).getName()); //must do it before actual removing + treeModel.removeNodeFromParent((DiagramModelTreeNode)typeNode.getChildAt(p.second)); + /* remove the bookmark keys associated with this property tree node, if any */ + for(String key : treeModel.getBookmarks()){ + treeModel.bookmarks.remove(key); + } + }else if("property.modifiers".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + @SuppressWarnings("unchecked") + Pair<String,Integer> p = (Pair<String,Integer>)evt.getSource(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,p.first); + PropertyMutableTreeNode propertyNode = ((PropertyMutableTreeNode)typeNode.getChildAt(p.second)); + StringBuilder builder = new StringBuilder(); + Modifiers modifiers = n.getProperties().getModifiers(p.first); + for(int index : modifiers.getIndexes(p.second)) + builder.append(modifiers.getTypes().get(index)).append(' '); + propertyNode.setModifierString(builder.toString()); + treeModel.nodeChanged(propertyNode); + }else if("arrowHead".equals(changeType)||"endLabel".equals(changeType)){ + /* source is considered to be the node whose end of the edge was changed */ + DiagramNode source = (DiagramNode)evt.getSource(); + DiagramEdge e = (DiagramEdge)evt.getDiagramElement(); + treeModel.nodeChanged(e); + for(int i=0; i<e.getChildCount();i++){ + NodeReferenceMutableTreeNode ref = (NodeReferenceMutableTreeNode)e.getChildAt(i); + if(ref.getNode() == source){ + treeModel.nodeChanged(ref); + iLog(("arrowHead".equals(changeType) ? "arrow head changed" :"end label changed"), + "edge:"+DiagramElement.toLogString(e)+" node:"+DiagramElement.toLogString(ref.getNode())+ + " value:"+ ("arrowHead".equals(changeType) ? e.getEndDescription(ref.getNode()): e.getEndLabel(ref.getNode()))); + break; + } + } + }else if("notes".equals(changeType)){ + /* do nothing as the tree update is taken care in the tree itself + * and cannot do it here because it must work for all the diagram tree nodes */ + + } + /* do nothing for other ElementChangedEvents as the position only concerns the diagram listeners */ + /* just forward the event to other listeners which might have been registered */ + diagramCollection.fireElementChanged(evt); + } + + private static DiagramModelTreeNode _lookForChild(DiagramModelTreeNode parentNode, String name){ + DiagramModelTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramModelTreeNode> children = parentNode.children(); children.hasMoreElements();){ + temp = children.nextElement(); + if(temp.getName().equals(name)){ + child = temp; + break; + } + } + return child; + } + + private static NodeReferenceMutableTreeNode _lookForNodeReference(DiagramEdge parent, DiagramNode n){ + NodeReferenceMutableTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramModelTreeNode> children = parent.children(); children.hasMoreElements();){ + temp = (NodeReferenceMutableTreeNode)children.nextElement(); + if( ((NodeReferenceMutableTreeNode)temp).getNode().equals(n)){ + child = temp; + break; + } + } + return child; + } + + private static EdgeReferenceMutableTreeNode _lookForEdgeReference( DiagramNode parentNode, DiagramEdge e){ + DiagramModelTreeNode edgeType = _lookForChild(parentNode, e.getType()); + assert(edgeType != null); + EdgeReferenceMutableTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramModelTreeNode> children = edgeType.children(); children.hasMoreElements();){ + temp = (EdgeReferenceMutableTreeNode)children.nextElement(); + if( ((EdgeReferenceMutableTreeNode)temp).getEdge().equals(e)){ + child = temp; + break; + } + } + return child; + } + + private void iLog(String action,String args){ + InteractionLog.log("MODEL",action,args); + } + + private DiagramModelTreeNode root; + InnerDiagramCollection diagramCollection; + private ArrayList<N> nodes; + private ArrayList<E> edges; + private ArrayList<DiagramElement> elements; + private InnerTreeModel treeModel; + + private long edgeCounter; + private long nodeCounter; + + private ElementNotifier notifier; + private List<ChangeListener> changeListeners; + + private boolean modified; + + private final static String ROOT_LABEL = "Diagram"; + private final static int INITIAL_EDGES_SIZE = 20; + private final static int INITIAL_NODES_SIZE = 30;}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModelTreeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,289 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +/** + * This class represent a general node in a TreeModel + * + */ +@SuppressWarnings("serial") +public abstract class DiagramModelTreeNode extends DefaultMutableTreeNode { + public DiagramModelTreeNode() { + super(); + notes = ""; + userObject = new UserObject(); + setSuperClassUserObject(userObject); + bookmarkKeys = new ArrayList<String>(); + } + + public DiagramModelTreeNode(Object userObject) { + this(); + setUserObject(userObject); + } + + /** + * Each DiagramModelTreeNode keeps track of the bookmarks it has been assigned. Bookmarks + * will affect how this tree node will be represented on a JTree: when a tree node is bookmarked + * an apex appears at the right of its name. + * @param key the bookmark + * @return true if this bookmark inner collection changed as a result of the call + */ + boolean addBookmarkKey(String key){ + return bookmarkKeys.add(key); + } + + /** + * Removes a bookmark key from the inner collection + * @param key the key to remove + * @return true if this bookmark inner collection changed as a result of the call + */ + boolean removeBookmarkKey(String key){ + return bookmarkKeys.remove(key); + } + + /** + * Returns the the bookmarks currently associated to this tree node + * @return + */ + public List<String> getBookmarkKeys(){ + return Collections.unmodifiableList(bookmarkKeys); + } + + public String getNotes(){ + return notes; + } + + /** + * Set a note for this tree node. A Note is a text the user wants to attach to a tree node. Notes + * will affect how this tree node will be represented on a JTree: when a tree node is assigned a note + * a number sign (#) appears at the right of its name. + * @param note the text of the note + * @return true if this bookmark inner collection changed as a result of the call + */ + void setNotes(String note){ + this.notes = note; + } + + /** + * @see DefaultMutableTreeNode#getParent() + */ + @Override + public DiagramModelTreeNode getParent(){ + return (DiagramModelTreeNode)super.getParent(); + } + + /** + * @see DefaultMutableTreeNode#getChildAt(int) + */ + @Override + public DiagramModelTreeNode getChildAt(int i){ + return (DiagramModelTreeNode)super.getChildAt(i); + } + + /** + * @see DefaultMutableTreeNode#getRoot() + */ + @Override + public DiagramModelTreeNode getRoot(){ + return (DiagramModelTreeNode)super.getRoot(); + } + + /** + * @see DefaultMutableTreeNode#setUserObject(Object) + */ + @Override + public void setUserObject(Object userObject){ + ((UserObject)this.userObject).setObject(userObject); + } + + /** + * @see DefaultMutableTreeNode#getUserObject() + */ + @Override + public Object getUserObject(){ + return userObject; + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + public String spokenText(){ + return ((UserObject)userObject).spokenText(); + } + + /** + * Returns a more detailed description of the tree node than {@link #spokenText()}. + * + * @return a description of the tree node + */ + public String detailedSpokenText(){ + return spokenText(); + } + + /** + * returns the tree node name "as it is", without any decoration such as notes, bookmarks or cardinality. + * Unlike the String returned by toString + * @return the tree node name + */ + public String getName(){ + return ((UserObject)userObject).getName(); + } + + /** + * @see DefaultMutableTreeNode#isRoot() + */ + @Override + public boolean isRoot(){ + return false; // root node overwrites this method + } + + /** + * @see DefaultMutableTreeNode#getLastLeaf() + */ + @Override + public DiagramModelTreeNode getLastLeaf() { + return (DiagramModelTreeNode)super.getLastLeaf(); + } + + /** + * @see DefaultMutableTreeNode#getNextLeaf() + */ + @Override + public DiagramModelTreeNode getNextLeaf() { + return (DiagramModelTreeNode)super.getNextLeaf(); + } + + /** + * @see DefaultMutableTreeNode#getNextNode() + */ + @Override + public DiagramModelTreeNode getNextNode() { + return (DiagramModelTreeNode)super.getNextNode(); + } + + /** + * @see DefaultMutableTreeNode#getNextSibling() + */ + @Override + public DiagramModelTreeNode getNextSibling() { + return (DiagramModelTreeNode)super.getNextSibling(); + } + + /** + * @see DefaultMutableTreeNode#getPreviousLeaf() + */ + @Override + public DiagramModelTreeNode getPreviousLeaf() { + return (DiagramModelTreeNode)super.getPreviousLeaf(); + } + + /** + * @see DefaultMutableTreeNode#getPreviousNode() + */ + @Override + public DiagramModelTreeNode getPreviousNode() { + return (DiagramModelTreeNode)super.getPreviousNode(); + } + + /** + * @see DefaultMutableTreeNode#getPreviousSibling() + */ + @Override + public DiagramModelTreeNode getPreviousSibling() { + return (DiagramModelTreeNode)super.getPreviousSibling(); + } + + private void setSuperClassUserObject(Object u){ + super.setUserObject(u); + } + + private UserObject getUserObjectInstance(){ + return new UserObject(); + } + + protected List<String> bookmarkKeys; + protected String notes; + /* hides the DefaultMutableTreeNode protected field */ + private Object userObject; + protected static final char NOTES_CHAR = '#'; + protected static final char BOOKMARK_CHAR = '\''; + protected static final String BOOKMARK_SPEAK = ", bookmarked"; + protected static final String NOTES_SPEAK = ", has notes"; + + @Override + public Object clone(){ + DiagramModelTreeNode clone = (DiagramModelTreeNode )super.clone(); + clone.notes = ""; + clone.bookmarkKeys = new ArrayList<String>(); + clone.userObject = clone.getUserObjectInstance(); + clone.setSuperClassUserObject(clone.userObject); + return clone; + } + + /* this works as a wrapper for the real user object in order to provide */ + /* decoration on the treeNode label to signal and/or bookmarks */ + private class UserObject { + private Object object; + + public UserObject(){ + object = ""; + } + public void setObject(Object o){ + this.object = o; + } + + @Override + public boolean equals(Object o){ + return this.object.equals(o); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(object.toString()); + if(!"".equals(notes)){ + builder.append(NOTES_CHAR); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_CHAR); + return builder.toString(); + } + + public String spokenText(){ + StringBuilder builder = new StringBuilder(object.toString()); + if(!"".equals(notes)){ + builder.append(NOTES_SPEAK); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_SPEAK); + return builder.toString(); + } + + public String getName(){ + return object.toString(); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,230 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.utils.Pair; + +/** + * This class represents a node in the diagram. + * + */ +@SuppressWarnings("serial") +public abstract class DiagramNode extends DiagramElement { + public DiagramNode(String type, NodeProperties properties){ + setType(type); + this.properties = properties; + } + + /** + * @see DiagramModelTreeNode#setNotes(String) + */ + @Override + void setNotes(String notes){ + this.notes = notes; + notifyChange(new ElementChangedEvent(this,this,"note")); + } + + /** + * Returns the properties of this node. Be aware that what is returned is the reference + * to the actual NodeProperties object inside this DiagramNode. Thereforemodifying the returned + * object will affect this node. + * @return the properties of this node + */ + public NodeProperties getProperties(){ + return properties; + } + + /** + * Returns a copy of the properties of this node. The modifying the returned object + * won't affect this Node. + * @return Returns a copy of the properties of this node + */ + public NodeProperties getPropertiesCopy(){ + NodeProperties p = (NodeProperties)properties.clone(); + for(String type : properties.getTypes()){ + Modifiers modifiers = properties.getModifiers(type); + int index = 0; + for(String value : properties.getValues(type)){ + if(properties.getModifiers(type).isNull()) + p.addValue(type, value); + else + p.addValue(type, value, modifiers.getIndexes(index++)); + } + } + return p; + } + + /** + * Set the NodeProperties of this node + * @param properties the properties to set for this node + */ + public void setProperties(NodeProperties properties){ + this.properties = properties; + notifyChange(new ElementChangedEvent(this.properties,this,"properties")); + } + + /** + * Add a property to the NodeProperties of this node + * @see NodeProperties#addValue(String, String) + * + */ + public void addProperty(String propertyType, String propertyValue){ + getProperties().addValue(propertyType, propertyValue); + int index = getProperties().getValues(propertyType).size() - 1; + notifyChange(new ElementChangedEvent(new Pair<String,Integer>(propertyType,index),this,"property.add")); + } + + /** + * Removes a property from the NodeProperties of this node + * @see NodeProperties#removeValue(String, int) + */ + public void removeProperty(String propertyType, int valueIndex){ + getProperties().removeValue(propertyType, valueIndex); + notifyChange(new ElementChangedEvent(new Pair<String,Integer>(propertyType,valueIndex),this,"property.remove")); + } + + /** + * Set a property on the NodeProperties of this node to a new value + * @see NodeProperties#setValue(String, int, String) + */ + public void setProperty(String propertyType, int valueIndex, String newValue){ + getProperties().setValue(propertyType, valueIndex, newValue); + notifyChange(new ElementChangedEvent(new Pair<String,Integer>(propertyType,valueIndex),this,"property.set")); + } + + /** + * Removes all the values in the NodeProperties of this node + * @see NodeProperties#clear() + */ + public void clearProperties(){ + getProperties().clear(); + notifyChange(new ElementChangedEvent(this,this,"properties.clear")); + } + + /** + * set the modifier indexes in the NodeProperties of this node + * @see Modifiers#setIndexes(int, Set) + */ + public void setModifierIndexes(String propertyType, int propertyValueIndex, Set<Integer> modifierIndexes){ + getProperties().getModifiers(propertyType).setIndexes(propertyValueIndex, modifierIndexes); + notifyChange(new ElementChangedEvent(new Pair<String,Integer>(propertyType,propertyValueIndex),this,"property.modifiers")); + } + + /** + * Returns a more detailed description of the node than {@link #spokenText()}. + * the description includes which how many properties the node has and + * how many edges are attached to it. + * + * @return a description of the node + */ + @Override + public String detailedSpokenText(){ + StringBuilder builder = new StringBuilder(getType()); + builder.append(' '); + builder.append(getName()); + builder.append('.').append(' '); + for(int i=0; i<getChildCount();i++){ + DiagramModelTreeNode treeNode = (DiagramModelTreeNode) getChildAt(i); + if(treeNode.getChildCount() > 0){ + builder.append(treeNode.getChildCount()) + .append(' ') + .append(treeNode.getName()) + .append(';') + .append(' '); + } + } + return builder.toString(); + } + + /** + * Returns the number of attached edges + * @return the number of attached edges + */ + public abstract int getEdgesNum(); + + /** + * Returns the attached edge at the specified index + * @param index an index into edge's list + * @return the attached edge at the specified index + */ + public abstract DiagramEdge getEdgeAt(int index); + + /** + * add an edge to the attached edges list + * @param the edge to be added + */ + public abstract boolean addEdge(DiagramEdge e); + + /** + * Removes an edge from the attached edges list + * @param the edge to be removed + */ + public abstract boolean removeEdge(DiagramEdge e); + + /** + * Gets the node this node is within. + * @return the parent node, or null if the node + * has no parent + */ + public abstract DiagramNode getExternalNode(); + + /** + * Sets the node this node is within. + * @param node the parent node, or null if the node + * has no parent + */ + public abstract void setExternalNode(DiagramNode node); + + /** + * Returns the number of internal nodes + * @return the number of internal nodes + */ + public abstract int getInternalNodesNum(); + + /** + * Gets the internal node at the specified index + * @param index an index into internal nodes list + * @return the internal node at the specified index + */ + public abstract DiagramNode getInternalNodeAt(int index); + + /** + * Adds a node to the internal nodes list. + * @param node the internal node to add + */ + public abstract void addInternalNode(DiagramNode node); + + /** + * Removes a node from the internal nodes list. + * @param node the internal node to remove + */ + public abstract void removeInternalNode(DiagramNode node); + + @Override + public Object clone(){ + DiagramNode clone = (DiagramNode)super.clone(); + clone.properties = (NodeProperties)properties.clone(); + return clone; + } + + private NodeProperties properties; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceHolderMutableTreeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,58 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * This class is a special tree node which holds the EdgeReferenceMutableTreeNode + * in the tree layout, where It is normally placed as a Node's child. + * + * + */ +@SuppressWarnings("serial") +public class EdgeReferenceHolderMutableTreeNode extends DiagramModelTreeNode { + + public EdgeReferenceHolderMutableTreeNode(Object userObj){ + super(userObj); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(super.toString()); + builder.append(" (").append(getChildCount()).append(")"); + return builder.toString(); + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + StringBuilder builder = new StringBuilder(getName()); + builder.append(", "); + builder.append(getChildCount() == 0 ? "empty" : getChildCount()); + if(!"".equals(notes)){ + builder.append(NOTES_SPEAK); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_SPEAK); + return builder.toString(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceMutableTreeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,88 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * + * The diagramModeltreeNode placed in a node subtree representing an edge connecting + * that node with another node. + * + */ +@SuppressWarnings("serial") +public class EdgeReferenceMutableTreeNode extends DiagramModelTreeNode { + public EdgeReferenceMutableTreeNode(DiagramEdge edge, DiagramNode node){ + super(); + this.edge = edge; + this.node = node; + } + + @Override + public String toString(){ + final String and = " and "; + StringBuilder b = new StringBuilder(); + b.append("to "); + for(int i=0;i<edge.getNodesNum();i++){ + DiagramNode n = edge.getNodeAt(i); + if(!n.equals(node)) + b.append(n.getName()).append(and); + } + // remove the last " and " + b.delete(b.length()-and.length(), b.length()); + b.append(", via "); + b.append(edge.getName()); + super.setUserObject(b.toString()); + return super.toString(); + } + + @Override + public String getName(){ + return toString(); + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + toString(); + return super.spokenText(); + } + + /** + * Returns the diagram edge that this tree node represents inside the node subtree + * @return a reference to the actual edge + */ + public DiagramEdge getEdge(){ + return edge; + } + + /** + * Returns the node containing this tree node in its subtree. Notice that + * diagram nodes are DiagrammodelTreeNode as well. + * @return a reference to the diagram node + */ + public DiagramNode getNode(){ + return node; + } + + private DiagramEdge edge; + private DiagramNode node; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ElementChangedEvent.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,58 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.EventObject; + +/** + * ElementChangedEvent is used to notify the model listeners that an element + * in the model has been changed (e.g. it has a new name). + * + */ +@SuppressWarnings("serial") +public class ElementChangedEvent extends EventObject { + + public ElementChangedEvent( Object source, DiagramElement element, String changeType) { + super(source); + this.changeType = changeType; + this.element = element; + } + + /** + * A String representing the change type. Subclasses of DiagramNode and DiagramEdge + * can throw their own events by passing as argument a String that describes the change. + * Such events will have no effect in the model but will be fired to all the registered ChangeEventListener. + * + * @return a String describing the change type + */ + public String getChangeType(){ + return changeType; + } + + /** + * Returns the DiagramElement that has been affected by this change + * @return the DiagramElement that has been affected by this change + */ + public DiagramElement getDiagramElement(){ + return element; + } + + private String changeType; + DiagramElement element; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ElementNotifier.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,30 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * An elementNotifier is used by a DiagramElement to make the model aware + * that it has been changed somehow (e.g. it has a new name). + * A Reference to the model's ElementNotifier is set into the DiagramElement + * as soon as it's inserted in the model + * + */ +public interface ElementNotifier { + void notifyChange(ElementChangedEvent evt); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/MalformedEdgeException.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,36 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * This RuntimeException is thrown when an edge is inserted in the model without being connected to + * any nodes before. + * + */ +@SuppressWarnings("serial") +public class MalformedEdgeException extends RuntimeException { + + public MalformedEdgeException(DiagramEdge edge, String message) { + super("Edge inserted into data structure was malformed for this reason:" + message); + } + + public MalformedEdgeException(Throwable arg0) { + super(arg0); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeProperties.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,527 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class represents the internal properties of a node. Internal properties can be seen as + * attributes that each single nodes owns and that, in a visual diagram, would normally be displayed inside + * or in the neighbourhood of the node itself. This is a very high abstraction of the concept of properties + * of a node as the user can define their own type of properties through the property type definition object + * passed as argument in the constructor. + * An example of properties is <i>attributes</i> and <i>methods</i> of a class diagram in the UML language or just the + * <i>attributes</i> of entities in an ER diagram. + * + */ +public final class NodeProperties implements Cloneable { + + /** + * Creates a diagram element property data structure out of a property type specification. In a UML + * diagram the NodeProperties for a class node would be constructed passing as arguments a linked hash + * map containing the strings "attributes" and "properties" and the strings "public", "protected", "private", + * "static" as value for both the keys. "attributes" and "methods" would be the types of the NodeProperties, + * that is, all the values inserted in the object such as values would fall under either type. For example + * if a UML diagram class has the methods getX() and getY(), these would be inserted in the NodeProperties + * object with a type "methods". + * + * @param typeDefinition a linked Hash Map holding the properties types as keys + * and the modifiers type definition of each property as values + */ + + public NodeProperties(LinkedHashMap<String,Set<String>> typeDefinition){ + if(typeDefinition == null) + this.typeDefinition = EMPTY_PROPERTY_TYPE_DEFINITION; + else + this.typeDefinition = typeDefinition; + /* create the type collection out of the typeDefinition keyset */ + types = Collections.unmodifiableList(new ArrayList<String>(this.typeDefinition.keySet())); + properties = new LinkedHashMap<String,Entry>(); + for(String s : types){ + Entry q = new Entry(); + q.values = new ArrayList<String>(); // property values to be filled by user + modifiers + q.indexes = new ArrayList<Set<Integer>>(); + q.view = null; + Set<String> modifierTypeDefinition = this.typeDefinition.get(s); + if(modifierTypeDefinition == null) + /* null modifiers for this property */ + q.modifiers = new Modifiers(EMPTY_MODIFIER_TYPE_DEFINITION,q.indexes); + else if(modifierTypeDefinition.size() == 0) + /* null modifiers for this property */ + q.modifiers = new Modifiers(EMPTY_MODIFIER_TYPE_DEFINITION,q.indexes); + else + q.modifiers = new Modifiers(modifierTypeDefinition, q.indexes); + properties.put(s, q); + } + } + + /** + * Returns the type definition argument of the constructor this object has been created through. + * + * @return the type definition + */ + public Map<String,Set<String>> getTypeDefinition(){ + return typeDefinition; + } + + /** + * Returns the types of properties. + * @return + */ + public List<String> getTypes(){ + return types; + } + + /** + * + * @param types the property type we want to get the values of. + * @return an array of string with the different properties set by the user or + * null if the specified type does not exist. + */ + public List<String> getValues(String propertyType){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + return new ArrayList<String>(e.values); + } + + /** + * Returns the view object associated with a property type in this NodeProperties instance. A view object is + * defined by the client of this class and it holds the information needed for a visual representation + * of the property. + * @param type the property type the returned view is associated with + * @return the View object or null if non has been set previously + */ + public Object getView(String type){ + Entry e = properties.get(type); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type); + return e.view; + } + + /** + * Sets the View object for the a type of properties. The NodeProperties only works + * as a holder as for the view objects. The client code of this class has to define + * its own view, which will then be used by + * @param type the type of property the view is associated with. + * @param o an object defined by user + * @see #getView(String) + */ + public void setView(String type, Object o){ + Entry e = properties.get(type); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type); + e.view = o; + } + + /** + * + * Returns a reference to the modifier object associated with a property type. Changes to the returned + * reference will affect the internal state of the NodeProperty object. + * + * @param propertyType a property type + * @return the modifiers of the specified property type or null if either + * the property type is null or it was not listed in the property type definition + * passed to the constructor + */ + public Modifiers getModifiers(String propertyType){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + return e.modifiers; + } + + /** + * Adds a value of the type passed as argument + * @param propertyType a property type defined in the property type definition passed as argument to the constructor + * @param propertyValue the value to add to the property type + */ + public void addValue(String propertyType, String propertyValue){ + Entry e = properties.get(propertyType); + /* no such property type exists */ + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.values.add(propertyValue); + e.indexes.add(new LinkedHashSet<Integer>()); + } + + /** + * Adds a value of the type passed as argument and sets the modifier indexes for it as for + * {@link Modifiers#setIndexes(int, Set)} + * @param propertyType a property type + * @param propertyValue a property value + * @param modifierIndexes the modifier set of indexes + * + * @throws IllegalArgumentException if propertyType + * is not among the ones in the type definition passed as argument to the constructor + */ + public void addValue(String propertyType, String propertyValue, Set<Integer> modifierIndexes){ + for(Integer i : modifierIndexes) + if((i < 0)||(i >= getModifiers(propertyType).getTypes().size())) + throw new ArrayIndexOutOfBoundsException("Index "+ i + + " corresponds to no Modifier type (modifierType size = "+ + getModifiers(propertyType).getTypes().size()+")" ); + + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.values.add(propertyValue); + e.indexes.add(modifierIndexes); + } + + /** + * Removes the value at the specified index for the specified property type + * @param propertyType a property type + * @param valueIndex the index of the value to remove + * @return the removed value + * @throws IllegalArgumentException if propertyType + * is not among the ones in the type definition passed as argument to the constructor + */ + public String removeValue(String propertyType, int valueIndex){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.indexes.remove(valueIndex); + return e.values.remove(valueIndex); + } + + /** + * Sets the value of a property type at the specified index to a new value + * @param propertyType a property type + * @param valueIndex the index of the value which must be replaced + * @param newValue the new value for the specified index + * @throws IllegalArgumentException if propertyType + * is not among the ones in the type definition passed as argument to the constructor + */ + public void setValue(String propertyType, int valueIndex, String newValue){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.values.set(valueIndex, newValue); + } + + /** + * Removes all the values and modifiers for a specific type + * @param propertyType the type whose property and modifiers must be removed + * @throws IllegalArgumentException if propertyType + * is not among the ones in the type definition passed as argument to the constructor + */ + public void clear(String propertyType){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.values.clear(); + e.indexes.clear(); + } + + /** + * Removes all the values and modifiers of this object. + */ + public void clear(){ + for(String type : types){ + clear(type); + } + } + + /** + * Returns true if this NodeProperties contains no values + * @return true if this NodeProperties contains no values + */ + public boolean isEmpty(){ + boolean empty = true; + for(String type : types) + if(!properties.get(type).values.isEmpty()){ + empty = false; + break; + } + return empty; + } + + /** + * Returns true if there are no values for the specified type in this NodeProperties instance + * @param propertyType + * @return true if there are no values for the specified type in this NodeProperties instance + */ + public boolean typeIsEmpty(String propertyType){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + return e.values.isEmpty(); + } + + /** + * true if this NodeProperties object has no types. This can happen if the constructor + * has been called with an empty or null property type definition + * @return true if this NodeProperties object has no types + */ + public boolean isNull(){ + return types.isEmpty(); + } + + /** + * Returns a string representation of types, value and modifiers of this property. Such a + * representation can be passed as argument to {@link #valueOf(String)} to populate a NodeProperties + * object. Such NodeProperties object though must have the same type and modifier definition of + * the object this method is called on. + * + * @return a string representation of the values and modifiers of this object + */ + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(String type : types){ + builder.append(TYPE_ENTRY_SEPARATOR).append(type); + int propertyValueIndex = 0; + for(String value : properties.get(type).values){ + builder.append(VALUE_ENTRY_SEPARATOR); + builder.append(value); + for(int modifierIndex : getModifiers(type).getIndexes(propertyValueIndex)){ + builder.append(MODIFIER_ENTRY_SEPARATOR); + builder.append(modifierIndex); + } + propertyValueIndex++; + } + } + return builder.toString(); + } + + /** + * Fills up the this property according to the string passed as arguments + * The string must be generated by calling toString on a NodeProeprty instance + * holding the same property types as type, otherwise an IllegalArgumentException + * is likely to be thrown. + * + * @param s the string representation of the property values, such as the one returned + * by toString() + * @throws IllegalArgumentException if s contains property types which + * are not among the ones in the type definition passed as argument to the constructor + */ + public void valueOf(String s){ + /* clear up previous values */ + clear(); + + /* a property entry is a string with the type of the property followed by value * + * entries of that type. all value entries begin with the VALUE_SEPARATOR. * + * a value entry is a property value followed by its modifiers entries, a modifier entry * + * is a modifier beginning with the MODIFIER_SEPARATOR */ + String propertyEntries[] = s.split(TYPE_ENTRY_SEPARATOR); + for(String propertyEntry : propertyEntries){ + String[] typeEntries = propertyEntry.split(VALUE_ENTRY_SEPARATOR); + String type = typeEntries[0]; + for(int i=1;i<typeEntries.length;i++){ + Entry e = properties.get(type); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type); + String valueEntries[] = typeEntries[i].split(MODIFIER_ENTRY_SEPARATOR); + String value = valueEntries[0]; + LinkedHashSet<Integer> modifiers = new LinkedHashSet<Integer>(); + for(int j=1;j<valueEntries.length;j++){ + modifiers.add(Integer.valueOf(valueEntries[j])); + } + addValue(type,value,modifiers); + } + } + } + + @Override + @SuppressWarnings(value = "unchecked") + public Object clone(){ + NodeProperties p = null; + try { + p = (NodeProperties)super.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + p.properties = (LinkedHashMap<String,Entry>)properties.clone(); + for(String key : p.properties.keySet()){ + Entry old = p.properties.get(key); + Entry q = new Entry(); + q.values = new ArrayList<String>(); + q.indexes = new ArrayList<Set<Integer>>(); + q.view = old.view; + if(old.modifiers != null){ + q.modifiers = new Modifiers(old.modifiers.modifierTypes, q.indexes); + for(String modifierType : q.modifiers.getTypes()) + q.modifiers.setView(modifierType, old.modifiers.getView(modifierType)); + } + p.properties.put(key, q); + } + return p; + } + + private class Entry{ + List<String> values; + List<Set<Integer>> indexes; + Modifiers modifiers; + Object view; + } + + /** + * A modifier is a label peculiar of a certain subset of properties. For example in + * a UML class diagram one or more methods can be labelled as <i>public</i>, <i>private</i> or <i>protected</i>. + * Had a NodeProperties instance been used to represent the methods of a class node, there would be then + * one modifier for each label: one for <i>public</i>, one for <i>private</i>, and one for <i>protected</i>. To each modifier + * a view-object can be associated, which describes how these labels would be rendered visually. + * Following on from the UML example, a view-object would be used with the protected modifier + * to hold the information that the properties which are assigned such a modifier must be prefixed + * with the '#' sign. + */ + public class Modifiers{ + private Modifiers(Set<String> modifierTypes, List<Set<Integer>> indexes){ + views = new LinkedHashMap<String,Object>(); + indexesRef = indexes; + this.modifierTypes = Collections.unmodifiableList(new ArrayList<String>(modifierTypes)); + for(String modifierType : modifierTypes){ + views.put(modifierType,null); + } + } + + /* only used by NodeProperties.clone() method */ + private Modifiers(List<String> modifierTypes, List<Set<Integer>> indexes){ + views = new LinkedHashMap<String,Object>(); + indexesRef = indexes; + this.modifierTypes = modifierTypes; + for(String modifierType : modifierTypes){ + views.put(modifierType,null); + } + } + + /** + * Returns the list of modifier types, as per the type definition passed as argument to the NodeProperties + * constructor. + * @return a list of modifier types + * @see NodeProperties#NodeProperties(LinkedHashMap) + */ + public List<String> getTypes(){ + return modifierTypes; + } + + /** + * Returns the view object associated with a modifier type in this Modifier instance. A view object is + * defined by the client of this class and it holds the information needed for a visual representation + * of the modifier. + * @param type the property type the returned view is associated with + * @return the View object or null if non has been set previously + */ + public Object getView(String modifierType){ + return views.get(modifierType); + } + + /** + * Sets the View object for the a type of modifier. The NodeProperties only works + * as a holder as for the view objects. The client code of this class has to define + * its own view, which will then be used by the client itself to visualise the modifier + * @param type the type of modifier the view is associated with. + * @param o an object defined by user + * @see #getView(String) + * @throws IllegalArgumentException if modifierType + * is not among the ones in the type definition passed as argument to the constructor + */ + public void setView(String modifierType, Object view){ + if(!views.containsKey(modifierType)) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+modifierType); + views.put(modifierType,view); + } + + /** + * Returns the modifier indexes for the specified property value. Each property type can be associated + * with one or more modifier type and each property value can be assigned one or more modifier. The + * integer set returned by this method tells the client code to which modifiers the property value at the index + * passed as argument is assigned. The returned indexes refer to the modifier list returned by {@link #getTypes()} + * + * @param propertyValueIndex + * @return a set of indexes of the modifier types the property value at the index passed as agument + * is assigned to + */ + public Set<Integer> getIndexes(int propertyValueIndex){ + Set<Integer> set = indexesRef.get(propertyValueIndex); + return (new LinkedHashSet<Integer>(set)); + } + + /** + * Set the modifier indexes for the property value at the index passed as argument + * @param propertyValueIndex the index of the property value + * @param modifierIndexes a set of indexes which refer to the list returned by {@link #getTypes()} + * @throws ArrayIndexOutOfBoundsException if one or more of the indexes in modifierIndexes are lower than 0 or greater + * or equal to the size of the modifier type list returned by {@link #getTypes()}. The same exception is also thrown + * if propertyValueIndex is lower than 0 or greater or equal to the property values list returned by {@link NodeProperties#getValues(String)} + * passing as argument the same property type passed to {@link NodeProperties#getModifiers(String)} to obtain + * this Modifiers instance. + * @see #getIndexes(int) + */ + public void setIndexes(int propertyValueIndex, Set<Integer> modifierIndexes){ + for(Integer i : modifierIndexes) + if((i < 0)||(i >= getTypes().size())) + throw new ArrayIndexOutOfBoundsException("Index "+ i + " corresponds to no Modifier Type (modifierType size = "+getTypes().size()+")" ); + + Set<Integer> m = indexesRef.get(propertyValueIndex); + m.clear(); + m.addAll(modifierIndexes); + } + + /** + * Removes all the modifier indexes for a property value at the specified index + * @param propertyValueIndex the index of the property value + */ + public void clear(int propertyValueIndex){ + Set<Integer> m = indexesRef.get(propertyValueIndex); + m.clear(); + } + + /** + * true if this Modifiers object has no types. This can happen if the constructor + * has been called with an empty or null modifier in the type definition passed as argument + * to the constructor of the NodeProperties object holding this Modifiers object + * @return true if this Modifiers object has no types + */ + public boolean isNull(){ + return modifierTypes.isEmpty(); + } + + private LinkedHashMap<String,Object> views; + private final List<String> modifierTypes; + private List<Set<Integer>> indexesRef; + } + + /* for each property (key) I associate a Couple (value) made out of * + * a list of the property values plus a list of possible modifiers */ + private LinkedHashMap<String,Entry> properties; + private final List<String> types; + private Map<String,Set<String>> typeDefinition; + + private static final LinkedHashMap<String,Set<String>> EMPTY_PROPERTY_TYPE_DEFINITION = new LinkedHashMap<String,Set<String>>(); + private static final Set<String> EMPTY_MODIFIER_TYPE_DEFINITION = Collections.emptySet(); + private static final String ILLEGAL_TYPE_MSG = "argument must be in type definition list: "; + /** + * A special static instance of the class corresponding to the null NodeProperties. A null NodeProperties instance will have isNull() returning true, + * which means it has no property types associated. + * @see isNull + */ + public static final NodeProperties NULL_PROPERTIES = new NodeProperties(EMPTY_PROPERTY_TYPE_DEFINITION); + + private final static String TYPE_ENTRY_SEPARATOR = "\n:"; + private final static String VALUE_ENTRY_SEPARATOR = "\n;"; + private final static String MODIFIER_ENTRY_SEPARATOR = "\n,"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeReferenceMutableTreeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,97 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * + * The diagramModeltreeNode placed in an edge subtree representing a node connected + * by the edge itself. + * + */ + +@SuppressWarnings("serial") +public class NodeReferenceMutableTreeNode extends DiagramModelTreeNode { + public NodeReferenceMutableTreeNode(DiagramNode node, DiagramEdge edge){ + super(); + this.node = node; + this.edge = edge; + } + + + @Override + public String toString(){ + StringBuilder b = new StringBuilder(); + if(edge.getEndDescription(node) != null){ + b.append(' '); + b.append(edge.getEndDescription(node)); + b.append(' '); + } + b.append(node.getName()); + b.append(' '); + if(edge.getEndLabel(node) != null){ + b.append(edge.getEndLabel(node)); + } + /* set the user object so that superclass toString can be called, which + * decorates the string with notes and bookmarks + */ + setUserObject(b.toString()); + return super.toString(); + } + + /** + * returns the tree node name "as it is", without any decoration such as notes, bookmarks or cardinality. + * Unlike the String returned by toString + * @return the tree node name + */ + @Override + public String getName(){ + return node.getName(); + } + + /** + * Returns a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + toString(); + return super.spokenText(); + } + + /** + * Returns the diagram edge that has this node in its subtree. Note that diagram edges + * are DiagramModelTreeNodes as well. + * @return a reference to the diagram edge + */ + public DiagramEdge getEdge(){ + return edge; + } + + /** + * Returns the diagram node that this tree node represents inside the edge subtree + * @return a reference to the actual diagram node + */ + public DiagramNode getNode(){ + return node; + } + + private DiagramNode node; + private DiagramEdge edge; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyMutableTreeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,64 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * This class represent node properties in the diagram tree representation. In the tree layout they are placed + * in the DiagramNode subtree. Note that a DiagramNode is also a DiagramModelTreeNode. + * @see NodeProperties + */ +@SuppressWarnings("serial") +public class PropertyMutableTreeNode extends DiagramModelTreeNode { + public PropertyMutableTreeNode(){ + super(); + } + + + public PropertyMutableTreeNode(Object userObject) { + super(userObject); + modifiersString = ""; + } + + /** + * Sets a string to show in the tree that the NodeProperties value this tree node refers to + * has been assigned one or more modifiers. + * + * @param s the modifier string + */ + public void setModifierString(String s){ + modifiersString = s; + } + + @Override + public String toString(){ + return modifiersString + super.toString(); + } + + /** + * Returns a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + return modifiersString + super.spokenText(); + } + + private String modifiersString; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyTypeMutableTreeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,101 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.List; +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; + +/** + * + * This DiagramModelTreeNode holds all the PropertyMutableTreeNodes of a certain type in the + * tree representation of the diagram. + * + * @see PropertyMutableTreeNode + * + */ +@SuppressWarnings("serial") +public class PropertyTypeMutableTreeNode extends DiagramModelTreeNode { + public PropertyTypeMutableTreeNode(String type, DiagramNode n){ + setUserObject(type); + node = n; + } + + public String getType(){ + return getName(); + } + + public DiagramNode getNode(){ + return node; + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(super.toString()); + builder.append(" (").append(getChildCount()).append(")"); + return builder.toString(); + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + StringBuilder builder = new StringBuilder(getName()); + builder.append(", "); + builder.append(getChildCount() == 0 ? "empty" : getChildCount()); + if(!"".equals(notes)){ + builder.append(NOTES_SPEAK); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_SPEAK); + return builder.toString(); + } + + /* creates and/or set the child treeNodes of this type with the property values */ + public void setValues(List<String> values, Modifiers modifiers){ + int diff = getChildCount() - values.size(); + if(diff > 0){ + for(int i=0;i<diff;i++) + remove(getChildCount()-1); + }else if(diff < 0){ + for(int i=0; i<-diff;i++) + add(new PropertyMutableTreeNode()); + } + + PropertyMutableTreeNode child; + for(int i=0; i<getChildCount();i++){ + StringBuilder builder = new StringBuilder(); + if(modifiers != null){ + Set<Integer> indexes = modifiers.getIndexes(i); + for(int index : indexes) + builder.append(modifiers.getTypes().get(index)).append(' '); + } + child = (PropertyMutableTreeNode)getChildAt(i); + child.setUserObject(values.get(i)); + child.setModifierString(builder.toString()); + } + } + + DiagramNode node; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TreeModel.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,133 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.Set; + +import javax.swing.event.ChangeListener; + +/** + * + * Represents the tree side of a DiagramModel instance. + * + * @param <N> a type extending DiagramNode + * @param <E> a type extending DiagramEdge + */ +public interface TreeModel<N extends DiagramNode, E extends DiagramEdge> extends javax.swing.tree.TreeModel { + + /** + * insert a DiagramNode into the diagram model + * + * @param treeNode the DiagramNode to be inserted in the collection + * @return true if the model changed as a result of the call + */ + boolean insertTreeNode(N treeNode); + + /** + * insert a DiagramEdge into the diagram model + * + * @param treeNode the DiagramEdge to be inserted in the collection + * @return true if the model changed as a result of the call + */ + boolean insertTreeNode(E treeNode); + + /** + * remove a DiagramElement from the model + * + * @param treeNode the diagramElement to be removed + * @return true if the model changed as a result of the call + */ + boolean takeTreeNodeOut(DiagramElement treeNode); + + /** + * + * Add a bookmark for the specified tree node in the internal collection + * + * @param bookmark a bookmark + * @param treeNode the tree node to be bookmarked + * @return previous value associated with specified key, or null if there was no mapping for key. + * @throws IllegalArgumentException if bookmark is null + */ + DiagramModelTreeNode putBookmark(String bookmark, DiagramModelTreeNode treeNode); + + /** + * Returns a bookmarked tree node + * @param bookmark the bookmark associated with the tree node + * @return the bookmarked tree node or null if no tree node was bookmarked with the argument + */ + DiagramModelTreeNode getBookmarkedTreeNode(String bookmark); + + /** + * Returns the list of all the bookmarks of this tree model + * @return the list of all the bookmarks + */ + Set<String> getBookmarks(); + + /** + * Remove the bookmark from the bookmark internal collection + * + * @param bookmark the bookmark to remove + * @return previous value associated with specified key, or null if there was no mapping for key. + */ + DiagramModelTreeNode removeBookmark(String bookmark); + + /** + * Set the notes for the specified tree node. Passing an empty string as notes + * means actually to remove the notes from the tree node. + * + * @param treeNode the tree node to be noted + * @param notes the notes to be assigned to the tree node + */ + void setNotes(DiagramModelTreeNode treeNode, String notes); + + /** + * Add a change listener to the model. the listeners will be fired each time the model + * goes from the unmodified to modified state. The model is modified when a either a + * node or an edge are inserted or removed or changed when they are within the model. + * @param l a ChangeListener to add to the model + */ + void addChangeListener(ChangeListener l); + + /** + * Removes a change listener from the model. + * @param l a ChangeListener to remove from the model + */ + void removeChangeListener(ChangeListener l); + + /** + * Returns true if the model has been modified + * @return true if the model has been modified + */ + boolean isModified(); + + /** + * Sets the model as unmodified. This entails that {@link #isModified()} will return + * false unless the model doesn't get modified again. After this call a new modification + * of the model would trigger the associated change listeners again. + */ + public void setUnmodified(); + + /** + * Returns an object that can be used to access the nodes and edges (via {@link #getNodes()} + * and {@link #getEdges()} in a synchronized block. The monitor is guaranteed to give + * exclusive access even in regards to the access via the tree side of the model. + * @return + */ + Object getMonitor(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TypeMutableTreeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,70 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * + * This class is a DiagramModelTreeNode representing a type of a diagram element (node or edge) + * in the diagram tree layout the elements will be children of this tree nodes. + * + */ +@SuppressWarnings("serial") +public class TypeMutableTreeNode extends DiagramModelTreeNode { + + public TypeMutableTreeNode(DiagramElement element) { + super(element.getType()); + this.prototype = element; + } + + /** + * Returns a prototype diagram element which can be cloned to create other diagram elements + * of this type. + * @return + */ + public DiagramElement getPrototype(){ + return (DiagramElement)prototype.clone(); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(super.toString()); + builder.append(" (").append(getChildCount()).append(")"); + return builder.toString(); + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + StringBuilder builder = new StringBuilder(getName()); + builder.append(", "); + builder.append(getChildCount() == 0 ? "empty" : getChildCount()); + if(!"".equals(notes)){ + builder.append(NOTES_SPEAK); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_SPEAK); + return builder.toString(); + } + + DiagramElement prototype; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Diagram.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,264 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PrototypePersistenceDelegate; + +/** + * This Diagram class holds all the data needed for a representation of the diagram. It is used by component classes + * such as {@link GraphPanel} and {@link DiagramTree} to draw the diagram by accessing the diagram model or by + * {@link EditorTabbedPane} to assign a title to the tabs out of the diagram name. + * + * + */ +public abstract class Diagram implements Cloneable { + + public static Diagram newInstance(String name, Node[] nodes, Edge[] edges, PrototypePersistenceDelegate prototypePersistenceDelegate){ + return new LocalDiagram(name,nodes,edges,prototypePersistenceDelegate); + } + + public abstract String getName(); + + public abstract void setName(String name); + + public abstract Node[] getNodePrototypes(); + + public abstract Edge[] getEdgePrototypes(); + + public abstract TreeModel<Node,Edge> getTreeModel(); + + public abstract CollectionModel<Node,Edge> getCollectionModel(); + + public abstract DiagramModelUpdater getModelUpdater(); + + public abstract String getLabel(); + + public abstract PrototypePersistenceDelegate getPrototypePersistenceDelegate(); + + @Override + public Object clone(){ + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + private static class LocalDiagram extends Diagram { + + private LocalDiagram(String name, Node[] nodes, Edge[] edges,PrototypePersistenceDelegate prototypePersistenceDelegate){ + this.name = name; + this.nodes = nodes; + this.edges = edges; + this.prototypePersistenceDelegate = prototypePersistenceDelegate; + diagramModel = new DiagramModel<Node,Edge>(nodes,edges); + innerModelUpdater = new InnerModelUpdater(); + } + + @Override + public String getName(){ + return name; + } + + @Override + public void setName(String name){ + this.name = name; + } + + @Override + public Node[] getNodePrototypes(){ + return nodes; + } + + @Override + public Edge[] getEdgePrototypes(){ + return edges; + } + + @Override + public TreeModel<Node,Edge> getTreeModel(){ + return diagramModel.getTreeModel(); + } + + @Override + public CollectionModel<Node,Edge> getCollectionModel(){ + return diagramModel.getDiagramCollection(); + } + + @Override + public String getLabel(){ + return name; + } + + @Override + public DiagramModelUpdater getModelUpdater(){ + return innerModelUpdater; + } + + @Override + public String toString(){ + return name; + } + + @Override + public PrototypePersistenceDelegate getPrototypePersistenceDelegate(){ + return prototypePersistenceDelegate; + } + + @Override + public Object clone(){ + LocalDiagram clone = (LocalDiagram)super.clone(); + clone.name = getName(); + clone.nodes = getNodePrototypes(); + clone.edges = getEdgePrototypes(); + /* constructor with no args makes just a dummy wrapper */ + clone.diagramModel = new DiagramModel<Node,Edge>(nodes,edges); + clone.innerModelUpdater = clone.new InnerModelUpdater(); + return clone; + } + + private DiagramModel<Node,Edge> diagramModel; + private InnerModelUpdater innerModelUpdater; + private PrototypePersistenceDelegate prototypePersistenceDelegate; + private String name; + private Node[] nodes; + private Edge[] edges; + + private class InnerModelUpdater implements DiagramModelUpdater { + + @Override + public boolean getLock(DiagramModelTreeNode treeNode, Lock lock) { + /* using a non shared diagram requires no actual lock, therefore the answer is always yes */ + return true; + } + + @Override + public void yieldLock(DiagramModelTreeNode treeNode, Lock lock) {} + + @Override + public void insertInCollection(DiagramElement element) { + if(element instanceof Node) + diagramModel.getDiagramCollection().insert((Node)element); + else + diagramModel.getDiagramCollection().insert((Edge)element); + } + + @Override + public void insertInTree(DiagramElement element) { + if(element instanceof Node) + diagramModel.getTreeModel().insertTreeNode((Node)element); + else + diagramModel.getTreeModel().insertTreeNode((Edge)element); + } + + @Override + public void takeOutFromCollection(DiagramElement element) { + diagramModel.getDiagramCollection().takeOut(element); + } + + @Override + public void takeOutFromTree(DiagramElement element) { + diagramModel.getDiagramCollection().takeOut(element); + } + + @Override + public void setName(DiagramElement element, String name) { + element.setName(name); + } + + @Override + public void setNotes(DiagramModelTreeNode treeNode, String notes) { + diagramModel.getTreeModel().setNotes(treeNode, notes); + } + + @Override + public void setProperty(Node node, String type, int index, + String value) { + node.setProperty(type, index, value); + } + + @Override + public void setProperties(Node node, NodeProperties properties) { + node.setProperties(properties); + } + + @Override + public void clearProperties(Node node) { + node.clearProperties(); + } + + @Override + public void addProperty(Node node, String type, String value) { + node.addProperty(type, value); + } + + @Override + public void removeProperty(Node node, String type, int index) { + node.removeProperty(type, index); + } + + @Override + public void setModifiers(Node node, String type, int index, + Set<Integer> modifiers) { + node.setModifierIndexes(type, index, modifiers); + } + + @Override + public void setEndLabel(Edge edge, Node node, String label) { + edge.setEndLabel(node, label); + } + + @Override + public void setEndDescription(Edge edge, Node node, + int index) { + edge.setEndDescription(node, index); + } + + @Override + public void translate(GraphElement ge, Point2D p, double x, double y) { + ge.translate(p, x, y); + } + + @Override + public void startMove(GraphElement ge, Point2D p) { + ge.startMove(p); + } + + @Override + public void bend(Edge edge, Point2D p) { + edge.bend(p); + } + + @Override + public void stopMove(GraphElement ge) { + ge.stopMove(); + } + } + } + +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramModelUpdater.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,80 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; + +/** + * + * The DiagramModelUpdater class is used to make changes to the diagram model. The reason why + * changes are not made directly to the model is allowing the network-local diagram interchangeability. + * A NetDiagram differs from a local + * diagram only by its DiagramModelUpdater implementation. The rest of the operations are + * performed through the delegate local diagram which is passed as argument to the constructor. + * In this way a local diagram can be easily turned into a network diagram and vice versa. + * + * @see NetDiagram + */ +public interface DiagramModelUpdater { + + public boolean getLock(DiagramModelTreeNode treeNode, Lock lock); + + public void yieldLock(DiagramModelTreeNode treeNode, Lock lock); + + public void insertInCollection(DiagramElement element); + + public void insertInTree(DiagramElement element); + + public void takeOutFromCollection(DiagramElement element); + + public void takeOutFromTree(DiagramElement element); + + public void setName(DiagramElement element, String name); + + public void setProperty(Node node, String type, int index, String value); + + public void setProperties(Node node, NodeProperties properties); + + public void clearProperties(Node node); + + public void setNotes(DiagramModelTreeNode treeNode, String notes); + + public void addProperty(Node node, String type, String value); + + public void removeProperty(Node node, String type, int index); + + public void setModifiers(Node node, String type, int index, Set<Integer> modifiers); + + public void setEndLabel(Edge edge, Node node, String label); + + public void setEndDescription(Edge edge, Node node, int i);// String description); + + public void translate(GraphElement ge, Point2D p, double x, double y); + + public void startMove(GraphElement ge, Point2D p); + + public void bend(Edge edge, Point2D p); + + public void stopMove(GraphElement ge); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramPanel.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,132 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.BorderLayout; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * It's the panel which displays a diagram. It contains a {@link GraphPanel}, a {@link DiagramTree} + * and a {@link GraphToolbar} + * + */ +@SuppressWarnings("serial") +public class DiagramPanel extends JPanel{ + + public DiagramPanel(Diagram diagram, EditorTabbedPane tabbedPane){ + this.diagram = diagram; + this.tabbedPane = tabbedPane; + setName(diagram.getLabel()); + setLayout(new BorderLayout()); + + modelChangeListener = new ChangeListener(){ + @Override + public void stateChanged(ChangeEvent e) { + setModified(true); + } + }; + + toolbar = new GraphToolbar(diagram); + graphPanel = new GraphPanel(diagram, toolbar); + /* the focus must be hold by the tree and the tab panel only */ + toolbar.setFocusable(false); + graphPanel.setFocusable(false); + + tree = new DiagramTree(diagram); + + /* the panel containing the graph and the toolbar */ + JPanel graphAndToolbarPanel = new JPanel(new BorderLayout()); + graphAndToolbarPanel.add(toolbar, BorderLayout.NORTH); + graphAndToolbarPanel.add(new JScrollPane(graphPanel),BorderLayout.CENTER); + + JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, + new JScrollPane(tree), + graphAndToolbarPanel); + splitPane.setDividerLocation((int)tree.getPreferredSize().width*2); + add(splitPane, BorderLayout.CENTER); + + diagram.getCollectionModel().addChangeListener(modelChangeListener); + } + + public String getFilePath(){ + return filePath; + } + + public void setFilePath(String newValue){ + filePath = newValue; + } + + public Diagram getDiagram(){ + return diagram; + } + + public void setDiagram(Diagram diagram){ + /* remove the listener from the old model */ + this.diagram.getCollectionModel().removeChangeListener(modelChangeListener); + diagram.getCollectionModel().addChangeListener(modelChangeListener); + + this.diagram = diagram; + tree.setDiagram(diagram); + graphPanel.setModelUpdater(diagram.getModelUpdater()); + setName(diagram.getLabel()); + /* set the * according to the new diagram's model modification status */ + setModified(isModified()); + } + + public GraphPanel getGraphPanel(){ + return graphPanel; + } + + public DiagramTree getTree(){ + return tree; + } + + /** This method is for changing the 'modified' status of the diagram. * + * When called passing false as argument listeners are notified that the * + * diagram has been saved. */ + public void setModified(boolean modified){ + if(!modified) + diagram.getCollectionModel().setUnmodified(); + /* add an asterisk to notify that the diagram has changed */ + if(modified) + setName(getName()+"*"); + else + setName(diagram.getLabel()); + tabbedPane.refreshComponentTabTitle(this); + } + + public boolean isModified(){ + return diagram.getCollectionModel().isModified(); + } + + private Diagram diagram; + private GraphPanel graphPanel; + private DiagramTree tree; + private GraphToolbar toolbar; + private String filePath; + private ChangeListener modelChangeListener; + private EditorTabbedPane tabbedPane; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramTree.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,723 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.io.InputStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.JOptionPane; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.EdgeReferenceMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeReferenceMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.TypeMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.Narrator; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +@SuppressWarnings("serial") +public class DiagramTree extends JTree { + public DiagramTree(Diagram diagram){ + super(diagram.getTreeModel()); + this.diagram = diagram; + resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + + TreePath rootPath = new TreePath((DiagramModelTreeNode)diagram.getTreeModel().getRoot()); + setSelectionPath(rootPath); + collapsePath(rootPath); + selectedNodes = new ArrayList<Node>(); + setEditable(false); + getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + overwriteTreeKeystrokes(); + this.addTreeSelectionListener(new TreeSelectionListener(){ + @Override + public void valueChanged(TreeSelectionEvent evt) { + if(treeSelectionListenerGateOpen){ + final DiagramModelTreeNode treeNode = (DiagramModelTreeNode)evt.getPath().getLastPathComponent(); + if(treeNode instanceof DiagramElement){ + SoundFactory.getInstance().play(((DiagramElement)treeNode).getSound(), new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(treeNode.spokenText()); + } + }); + }else{ + NarratorFactory.getInstance().speak(treeNode.spokenText()); + } + } + } + }); + /* don't use the swing focus system as we provide one on our own */ + setFocusTraversalKeysEnabled(false); + getAccessibleContext().setAccessibleName(""); + } + + @SuppressWarnings("unchecked") + @Override + public TreeModel<Node,Edge> getModel(){ + return (TreeModel<Node,Edge>)super.getModel(); + } + + public void setModel(TreeModel<Node,Edge> newModel){ + DiagramModelTreeNode selectedTreeNode = (DiagramModelTreeNode)getSelectionPath().getLastPathComponent(); + super.setModel(newModel); + collapseRow(0); + setSelectionPath(new TreePath(selectedTreeNode.getPath())); + } + + public void setDiagram(Diagram diagram){ + this.diagram = diagram; + setModel(diagram.getTreeModel()); + } + + public void selectNode(final Node n){ + selectedNodes.add(n); + treeModel.valueForPathChanged(new TreePath(n.getPath()),n.getName()); + + SoundFactory.getInstance().play(SoundEvent.OK,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.node_selected"),n.spokenText())); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"node selected for edge",n.getName()); + } + + public void unselectNode(final Node n){ + selectedNodes.remove(n); + treeModel.valueForPathChanged(new TreePath(n.getPath()),n.getName()); + + SoundFactory.getInstance().play(SoundEvent.OK,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.node_unselected"),n.spokenText())); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"node unselected for edge",DiagramElement.toLogString(n)); + } + + public DiagramNode[] getSelectedNodes(){ + DiagramNode[] array = new DiagramNode[selectedNodes.size()]; + return selectedNodes.toArray(array); + } + + public void clearNodeSelections(){ + ArrayList<Node> tempList = new ArrayList<Node>(selectedNodes); + selectedNodes.clear(); + for(Node n : tempList){ + treeModel.valueForPathChanged(new TreePath(n.getPath()),n.getName()); + diagram.getModelUpdater().yieldLock(n, Lock.MUST_EXIST); + } + } + + public String currentPathSpeech(){ + TreePath path = getSelectionPath(); + DiagramModelTreeNode selectedPathTreeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + if(selectedNodes.contains(selectedPathTreeNode)) + /* add information about the fact that the node is selected */ + return MessageFormat.format(resources.getString("speech.node_selected"), selectedPathTreeNode.spokenText()); + else + return selectedPathTreeNode.spokenText(); + } + + /** + * this method changes the selected tree node from an edge/node reference + * to the related edge/node itself + */ + public void jump(JumpTo jumpTo){ + final Narrator narrator = NarratorFactory.getInstance(); + TreePath oldPath; + treeSelectionListenerGateOpen = false; + switch(jumpTo){ + case REFERENCE : + oldPath = getSelectionPath(); + DiagramModelTreeNode selectedTreeNode = (DiagramModelTreeNode)oldPath.getLastPathComponent(); + if(selectedTreeNode instanceof NodeReferenceMutableTreeNode){ + final Node n = (Node)((NodeReferenceMutableTreeNode)selectedTreeNode).getNode(); + setSelectionPath(new TreePath(n.getPath())); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(MessageFormat.format(resources.getString("speech.jump"),n.spokenText())); + } + }, SoundEvent.JUMP); + }else if(selectedTreeNode instanceof EdgeReferenceMutableTreeNode){ + final Edge e = (Edge)((EdgeReferenceMutableTreeNode)selectedTreeNode).getEdge(); + setSelectionPath(new TreePath(e.getPath())); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(MessageFormat.format(resources.getString("speech.jump"),e.spokenText())); + } + }, SoundEvent.JUMP); + } + /* assume the referee has only root in common with the reference and collapse everything up to the root (excluded) */ + collapseAll(selectedTreeNode, (DiagramModelTreeNode)selectedTreeNode.getPath()[1]); + break; + case ROOT : + final DiagramModelTreeNode from =(DiagramModelTreeNode)getSelectionPath().getLastPathComponent(); + setSelectionRow(0); + collapseAll(from,from.getRoot()); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(MessageFormat.format(resources.getString("speech.jump"),from.getRoot().spokenText())); + } + }, SoundEvent.JUMP); + break; + case TYPE : // jumps to the ancestor type node of the current node, never used + oldPath = getSelectionPath(); + int index = 0; + Object[] pathComponents = oldPath.getPath(); + for(int i=0;i<pathComponents.length;i++){ + if(pathComponents[i] instanceof TypeMutableTreeNode){ + index=i; + break; + } + } + final DiagramModelTreeNode typeTreeNode = (DiagramModelTreeNode)oldPath.getPathComponent(index); + setSelectionPath(new TreePath(typeTreeNode.getPath())); + collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(),typeTreeNode); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(MessageFormat.format(resources.getString("speech.jump"),typeTreeNode.spokenText())); + } + }, SoundEvent.JUMP); + break; + case SELECTED_TYPE : + DiagramModelTreeNode root = (DiagramModelTreeNode)getModel().getRoot(); + Object[] types = new Object[root.getChildCount()]; + for(int i=0; i< root.getChildCount();i++) + types[i] = ((TypeMutableTreeNode)root.getChildAt(i)).getName();//not to spokenText as it would be too long + oldPath = getSelectionPath(); + /* initial value is the type node whose branch node is currently selected */ + /* it is set as the first choice in the selection dialog */ + Object initialValue; + if(oldPath.getPath().length < 2) + initialValue = types[0]; + else + initialValue = oldPath.getPathComponent(1);//type tree node + /* the selection from the OptionPane is the stering returned by getName() */ + InteractionLog.log(INTERACTIONLOG_SOURCE,"open select type to jump dialog",""); + final String selectedValue = (String)SpeechOptionPane.showSelectionDialog( + SpeechOptionPane.getFrameForComponent(this), + "select type to jump to", + types, + initialValue); + if(selectedValue == null){ + /* it speaks anyway as we set up the playerListener in the EditorFrame class. No need to use narrator then */ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + InteractionLog.log(INTERACTIONLOG_SOURCE,"cancel select type to jump dialog",""); + treeSelectionListenerGateOpen = true; + return; + } + /* we search in the root which type tree node has getName() equal to the selected one */ + TypeMutableTreeNode typeNode = null; + for(int i = 0; i< root.getChildCount(); i++){ + TypeMutableTreeNode temp = (TypeMutableTreeNode)root.getChildAt(i); + if(temp.getName().equals(selectedValue)){ + typeNode = temp; + break; + } + } + setSelectionPath(new TreePath(typeNode.getPath())); + if(oldPath.getPath().length >= 2) + collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(), (DiagramModelTreeNode)initialValue); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(MessageFormat.format(resources.getString("speech.jump"),selectedValue)); + } + }, SoundEvent.JUMP); + break; + case BOOKMARK : + TreeModel<Node,Edge> treeModel = getModel(); + + if(treeModel.getBookmarks().size() == 0){ + SoundFactory.getInstance().play(SoundEvent.ERROR ,new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak("speech.no_bookmarks"); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"no bookmarks available",""); + treeSelectionListenerGateOpen = true; + return; + } + + String[] bookmarkArray = new String[treeModel.getBookmarks().size()]; + bookmarkArray = treeModel.getBookmarks().toArray(bookmarkArray); + + InteractionLog.log(INTERACTIONLOG_SOURCE,"open select bookmark dialog",""); + String bookmark = (String)SpeechOptionPane.showSelectionDialog( + JOptionPane.getFrameForComponent(this), + "Select bookmark", + bookmarkArray, + bookmarkArray[0] + ); + + if(bookmark != null){ + oldPath = getSelectionPath(); + DiagramModelTreeNode treeNode = treeModel.getBookmarkedTreeNode(bookmark); + collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(), (DiagramModelTreeNode)treeModel.getRoot()); + setSelectionPath(new TreePath(treeNode.getPath())); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(currentPathSpeech); + } + }, SoundEvent.JUMP); + InteractionLog.log(INTERACTIONLOG_SOURCE,"bookmark selected",bookmark); + }else{ + /* it speaks anyway, as we set up the speech in the EditorFrame class. no need to use the narrator then */ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + InteractionLog.log(INTERACTIONLOG_SOURCE,"cancel select bookmark dialog",""); + treeSelectionListenerGateOpen = true; + return; + } + break; + + } + InteractionLog.log(INTERACTIONLOG_SOURCE,"jumped to "+jumpTo.toString(),((DiagramModelTreeNode)getSelectionPath().getLastPathComponent()).getName()); + SoundFactory.getInstance().play(SoundEvent.JUMP); + treeSelectionListenerGateOpen = true; + } + + public void jumpTo(final DiagramElement de){ + TreePath oldPath = getSelectionPath(); + collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(),de); + setSelectionPath(new TreePath(de.getPath())); + treeSelectionListenerGateOpen = false; + SoundFactory.getInstance().play( SoundEvent.JUMP, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.jump"),de.spokenText())); + } + }); + treeSelectionListenerGateOpen = true; + } + + /* collapse all the nodes in the path from "from" to "to" upwards(with the same direction as going from a leaf to the root)*/ + private void collapseAll(DiagramModelTreeNode from, DiagramModelTreeNode to){ + DiagramModelTreeNode currentNode = from; + while(currentNode.getParent() != null && currentNode != to){ + currentNode = currentNode.getParent(); + collapsePath(new TreePath(currentNode.getPath())); + } + } + + @Override + protected void processMouseEvent(MouseEvent e){ + //do nothing as the tree does not have to be editable with mouse + } + + private void overwriteTreeKeystrokes() { + /* overwrite the keys. up and down arrow are overwritten so that it loops when the top and the */ + /* bottom are reached rather than getting stuck */ + + /* Overwrite keystrokes up,down,left,right arrows and space, shift, ctrl */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0),"down"); + getActionMap().put("down", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)getLastSelectedPathComponent(); + /* look if we've got a sibling node after (we are not at the bottom) */ + DiagramModelTreeNode nextTreeNode = treeNode.getNextSibling(); + SoundEvent loop = null; + if(nextTreeNode == null){ + DiagramModelTreeNode parent = treeNode.getParent(); + if(parent == null) /* root node, just stay there */ + nextTreeNode = treeNode; + else /* loop = go to first child of own parent */ + nextTreeNode = (DiagramModelTreeNode)parent.getFirstChild(); + loop = SoundEvent.LIST_BOTTOM_REACHED; + } + treeSelectionListenerGateOpen = false; + setSelectionPath(new TreePath(nextTreeNode.getPath())); + final InputStream finalSound = getTreeNodeSound(nextTreeNode); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + SoundFactory.getInstance().play(finalSound); + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + treeSelectionListenerGateOpen = true; + InteractionLog.log(INTERACTIONLOG_SOURCE,"move down",nextTreeNode.toString()); + }}); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0),"up"); + getActionMap().put("up", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)getLastSelectedPathComponent(); + DiagramModelTreeNode previousTreeNode = treeNode.getPreviousSibling(); + SoundEvent loop = null; + if(previousTreeNode == null){ + DiagramModelTreeNode parent = treeNode.getParent(); + if(parent == null) /* root node */ + previousTreeNode = treeNode; + else + previousTreeNode = (DiagramModelTreeNode)parent.getLastChild(); + loop = SoundEvent.LIST_TOP_REACHED; + } + treeSelectionListenerGateOpen = false; + setSelectionPath(new TreePath(previousTreeNode.getPath())); + final InputStream finalSound = getTreeNodeSound(previousTreeNode); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + SoundFactory.getInstance().play(finalSound); + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + treeSelectionListenerGateOpen = true; + InteractionLog.log(INTERACTIONLOG_SOURCE,"move up",previousTreeNode.toString()); + }}); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0),"right"); + getActionMap().put("right", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + if(treeNode.isLeaf()){ + notifyBorderReached(treeNode); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move right","border reached"); + } + else{ + treeSelectionListenerGateOpen = false; + expandPath(path); + setSelectionPath(new TreePath(((DiagramModelTreeNode)treeNode.getFirstChild()).getPath())); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_EXPAND,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + treeSelectionListenerGateOpen = true; + InteractionLog.log(INTERACTIONLOG_SOURCE,"move right",((DiagramModelTreeNode)treeNode.getFirstChild()).toString()); + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0),"left"); + getActionMap().put("left", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + DiagramModelTreeNode parent = treeNode.getParent(); + if(parent == null){/* root node */ + notifyBorderReached(treeNode); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move left","border reached"); + } + else{ + treeSelectionListenerGateOpen = false; + TreePath newPath = new TreePath(((DiagramModelTreeNode)parent).getPath()); + setSelectionPath(newPath); + collapsePath(newPath); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_COLLAPSE,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move left",((DiagramModelTreeNode)parent).toString()); + treeSelectionListenerGateOpen = true; + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,0),"space"); + getActionMap().put("space",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + NarratorFactory.getInstance().speak(currentPathSpeech()); + InteractionLog.log(INTERACTIONLOG_SOURCE,"info requested",""); + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,InputEvent.CTRL_DOWN_MASK),"ctrlspace"); + getActionMap().put("ctrlspace",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + /*//this code snippet reads out the whole path from the root to the selected node + * StringBuilder builder = new StringBuilder(); + * TreePath path = getSelectionPath(); + * for(Object o : path.getPath()){ + * builder.append(((DiagramModelTreeNode)o).spokenText()); + * builder.append(", "); + * } + * Narrator.getInstance().speak(builder.toString(), null); + */ + TreePath path = getSelectionPath(); + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + NarratorFactory.getInstance().speak(treeNode.detailedSpokenText()); + InteractionLog.log(INTERACTIONLOG_SOURCE,"detailed info requested",""); + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT,InputEvent.SHIFT_DOWN_MASK),"shift"); + getActionMap().put("shift",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + if(getSelectionPath().getLastPathComponent() instanceof Node){ + Node node = (Node)getSelectionPath().getLastPathComponent(); + + + if(selectedNodes.contains(node)){ + unselectNode(node); + diagram.getModelUpdater().yieldLock(node, Lock.MUST_EXIST); + } + else{ + if(!diagram.getModelUpdater().getLock(node, Lock.MUST_EXIST)){ + InteractionLog.log(INTERACTIONLOG_SOURCE,"Could not get lock on node fro edge creation selection",DiagramElement.toLogString(node)); + SpeechOptionPane.showMessageDialog( + SpeechOptionPane.getFrameForComponent(DiagramTree.this), + resources.getString("dialog.lock_failure.must_exist"), + SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech()); + } + }); + return; + } + selectNode(node); + } + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"ctrldown"); + getActionMap().put("ctrldown",SpeechUtilities.getShutUpAction()); + + /* make the tree ignore the page up and page down keys */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,0),"none"); + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,0),"none"); + } + + private static InputStream getTreeNodeSound(DiagramModelTreeNode node){ + InputStream sound = null; + TreeNode[] newPath = node.getPath(); + if(!node.isRoot()){ + if(node.getChildCount() > 0){ // check whether it's the folder containing Node/Edge references + if(node.getChildAt(0) instanceof EdgeReferenceMutableTreeNode){ + sound = ((EdgeReferenceMutableTreeNode)node.getChildAt(0)).getEdge().getSound(); + }else{ + sound = ((TypeMutableTreeNode)newPath[1]).getPrototype().getSound(); + } + }else{ + if(node instanceof NodeReferenceMutableTreeNode){ + sound = ((NodeReferenceMutableTreeNode)node).getNode().getSound(); + }else if(node instanceof EdgeReferenceMutableTreeNode){ + sound = ((EdgeReferenceMutableTreeNode)node).getNode().getSound(); + }else{ + sound = ((TypeMutableTreeNode)newPath[1]).getPrototype().getSound(); + } + } + } + return sound; + } + + @Override + public void setSelectionPath(TreePath path){ + super.setSelectionPath(path); + scrollPathToVisible(path); + } + + private void notifyBorderReached(DiagramModelTreeNode n) { + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + + @Override + public String convertValueToText(Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus){ + StringBuilder builder = new StringBuilder(super.convertValueToText(value, selected, expanded, leaf, row, hasFocus)); + if(selectedNodes != null) + if(selectedNodes.contains(value)){ + builder.insert(0, SELECTED_NODE_MARK_BEGIN); + builder.append(SELECTED_NODE_MARK_END); + } + return builder.toString(); + } + + @Override + protected TreeModelListener createTreeModelListener(){ + return new DiagramTreeModelHandler(); + } + + + private List<Node> selectedNodes; + private Diagram diagram; + private ResourceBundle resources; + private boolean treeSelectionListenerGateOpen; + private static final char SELECTED_NODE_MARK_BEGIN = '<'; + private static final char SELECTED_NODE_MARK_END = '>'; + private static final String INTERACTIONLOG_SOURCE = "TREE"; + static enum JumpTo {REFERENCE, ROOT, TYPE, SELECTED_TYPE, BOOKMARK} + + /* the methods of the TreeModelHandler are overwritten in order to provide a consistent way + * of updating the tree selection upon tree change. Bear in mind that the tree can possibly be changed + * by another peer on a network, and therefore not only as a response to a user's action. + * The algorithm works as follows (being A the tree node selected before any handler method M being called): + * + * if A ain't deleted as a result of M : do nothing + * if A's deleted as a result of M's execution : say A was the n-th sibling select the new n-th sibling + * or, if now the sibling nodes are less than n, select the one with highest index + * if no sibling nodes are still connected to the tree select the parent or the closest ancestor connected to the tree + */ + private class DiagramTreeModelHandler extends JTree.TreeModelHandler{ + + @Override + public void treeStructureChanged(final TreeModelEvent e) { + /* check first if what we're removing is in the selection path */ + TreePath path = e.getTreePath(); + boolean isInSelectionPath = false; + for(Object t : getSelectionPath().getPath()){ + if(path.getLastPathComponent() == t){ + isInSelectionPath = true; + break; + } + } + + if(isInSelectionPath){ + Object[] pathArray = getSelectionPath().getPath(); + DefaultMutableTreeNode root = (DefaultMutableTreeNode)getModel().getRoot(); + /* go along the path from the selected node to the root looking for a node * + * attached to the tree or with sibling nodes attached to the tree */ + for(int i=pathArray.length-1;i>=0;i--){ + DiagramModelTreeNode onPathTreeNode = (DiagramModelTreeNode)pathArray[i]; + if(onPathTreeNode.isNodeRelated(root)){// if can reach the root from here a.k.a. the node is still part of the tree + super.treeStructureChanged(e); + setSelectionPath(new TreePath(onPathTreeNode.getPath())); + break; + }else{ + /* check sibling nodes*/ + DefaultMutableTreeNode parent = (DiagramModelTreeNode)pathArray[i-1]; + if(parent.isNodeRelated(root) && parent.getChildCount() > 0){ + super.treeStructureChanged(e); + setSelectionPath(new TreePath(((DefaultMutableTreeNode)parent.getLastChild()).getPath())); + break; + } + } + } + }else + super.treeStructureChanged(e); + repaint(); + } + + @Override + public void treeNodesChanged(final TreeModelEvent e){ + TreePath path = getSelectionPath(); + super.treeNodesChanged(e); + setSelectionPath(path); + } + + @Override + public void treeNodesRemoved(final TreeModelEvent e){ + /* check first if what we're removing is in the selecton path */ + TreePath path = e.getTreePath(); + DiagramModelTreeNode removedTreeNode = (DiagramModelTreeNode)e.getChildren()[0]; + boolean isInSelectionPath = false; + for(Object t : getSelectionPath().getPath()){ + if(removedTreeNode == t){ + isInSelectionPath = true; + break; + } + } + DiagramModelTreeNode parentTreeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + /* update the selection only if the tree node involved is in the selection path * + * this always holds true for tree nodes deleted from the tree */ + if(isInSelectionPath){ + if(e.getSource() instanceof TreeModel){ + /* update the path only if the node has been removed from the tree or * + * if the currently selected tree node is going to be removed by this action * + * Need to call collapsePath only if the source of the deletion is the tree * + * as otherwise the selected node is always a leaf */ + collapsePath(path); + setSelectionPath(path); + }else{ + /* if we deleted from another source, then select the first non null node in the path * + * including the deleted node. E.g. if we're deleting the first child of a parent * + * and the node has siblings than the new first sibling will be selected */ + int limitForParentDeletion = (parentTreeNode instanceof Edge) ? 1 : 0; // an edge with one node is to be deleted + if(parentTreeNode.getChildCount() > limitForParentDeletion){ + setSelectionPath(new TreePath(((DiagramModelTreeNode)parentTreeNode.getChildAt( + /* select the n-th sibling node (see algorithm description above or the highest index sibling node */ + Math.min(e.getChildIndices()[0],parentTreeNode.getChildCount()-1) + )).getPath())); + }else{ + /* the deleted node had no siblings, thus select the node checking from the parent up in the path to the first still existing node */ + Object[] pathArray = path.getPath(); + for(int i=path.getPathCount()-1;i>=0;i--){ + DiagramModelTreeNode itr = (DiagramModelTreeNode)pathArray[i]; + if(itr.getPath()[0] == getModel().getRoot()){ + TreePath newPath = new TreePath(itr.getPath()); + setSelectionPath(newPath); + collapsePath(newPath); + break; + } + } + } + } + }else + super.treeNodesRemoved(e); + + /* if the node was selected for edge creation, then remove it from the list */ + DiagramModelTreeNode removedNode = (DiagramModelTreeNode)e.getChildren()[0]; + selectedNodes.remove(removedNode); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Direction.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,138 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; + +/** + This class describes a direction in the 2D plane. + A direction is a vector of length 1 with an angle between 0 + (inclusive) and 360 degrees (exclusive). There is also + a degenerate direction of length 0. +*/ +public class Direction +{ + /** + Constructs a direction (normalized to length 1). + @param dx the x-value of the direction + @param dy the corresponding y-value of the direction + */ + public Direction(double dx, double dy) + { + x = dx; + y = dy; + double length = Math.sqrt(x * x + y * y); + if (length == 0) return; + x = x / length; + y = y / length; + } + + /** + Constructs a direction between two points + @param p the starting point + @param q the ending point + */ + public Direction(Point2D p, Point2D q) + { + this(q.getX() - p.getX(), + q.getY() - p.getY()); + } + + public boolean isParallel(Direction d){ + if(equals(d.x,d.y,DELTA)||turn(180).equals(d.x,d.y,DELTA)) + return true; + else + return false; + } + + /** + Turns this direction by an angle. + @param angle the angle in degrees + */ + public Direction turn(double angle){ + double a = Math.toRadians(angle); + return new Direction( + x * Math.cos(a) - y * Math.sin(a), + x * Math.sin(a) + y * Math.cos(a)); + } + + /** + Gets the x-component of this direction + @return the x-component (between -1 and 1) + */ + public double getX() + { + return x; + } + + /** + Gets the y-component of this direction + @return the y-component (between -1 and 1) + */ + public double getY() + { + return y; + } + + private boolean equals(double dx, double dy ){ + return ((x==dx)&&(y==dy)); + } + + private boolean equals(double dx, double dy , double delta){ + return ((Math.abs(x-dx)<delta)&&(Math.abs(y-dy)<delta)); + } + + @Override + public String toString(){ + return "("+x+","+y+")"; + } + + private double x; + private double y; + + private static final double DELTA = 0.05; + + public static final Direction NORTH = new Direction(0, -1); + public static final Direction SOUTH = new Direction(0, 1); + public static final Direction EAST = new Direction(1, 0); + public static final Direction WEST = new Direction(-1, 0); + public static final Direction NONE = new Direction(0, 0); + + public static Direction compute(Point2D p, Point2D q){ + double x,y; + x = p.getX() - q.getX(); + y = p.getY() - q.getY(); + double length = Math.sqrt(x * x + y * y); + if (length == 0) + return NONE; + x = x / length; + y = y / length; + if(NORTH.equals(x, y)) + return NORTH; + if(SOUTH.equals(x, y)) + return SOUTH; + if(EAST.equals(x, y)) + return EAST; + if(WEST.equals(x, y)) + return WEST; + return NONE; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,679 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.ResourceBundle; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramEdge; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; + +/** + * An edge in a graph. Edge objects are used in a GraphPanel to render a diagram edge visually. + * Edge objects are used in the tree representation of the diagram as well, as they're + * subclasses of {@link DiagramEdge} + * + */ +@SuppressWarnings("serial") +public abstract class Edge extends DiagramEdge implements GraphElement{ + + public Edge(String type, String[] availableEndDescriptions, int minAttachedNodes, int maxAttachedNodes,LineStyle style){ + super(type,availableEndDescriptions); + this.minAttachedNodes = minAttachedNodes; + this.maxAttachedNodes = maxAttachedNodes; + this.style = style; + nodes = new ArrayList<Node>(); + } + + @Override + public Node getNodeAt(int index){ + return nodes.get(index); + } + + @Override + public int getNodesNum(){ + return nodes.size(); + } + + public boolean removeNode(DiagramNode diagramNode){ + Node n = (Node)diagramNode; + if(nodes.size() == 2) + throw new RuntimeException("Cannot remove a node from a two ends edge"); + else{ + for(InnerPoint p : points) + if(p.hasNeighbour(n)){ + p.neighbours.remove(n); + if(p.neighbours.size() == 1) + removePoint(p); + break; + } + return nodes.remove(n); + } + } + + /** + * Look for the node attached to this edge which lays at the minimum distance + * from the point passed as argument. The distance cannot be lower than the + * value passed as argument. + * + * @param aPoint the point the distance is measured from + * @param distanceLimit the limit from the distance between the nodes and the point + * @return the closest node or null if the node lays at an higher distance than distanceLimit + */ + public Node getClosestNode(Point2D aPoint, double distanceLimit){ + Node closestNode = null; + double minDist = distanceLimit; + + if(points.isEmpty()){ + Line2D line = getSegment(nodes.get(0),nodes.get(1)); + if(line.getP1().distance(aPoint) < minDist){ + minDist = line.getP1().distance(aPoint); + closestNode = nodes.get(0); + } + if(line.getP2().distance(aPoint) < minDist){ + minDist = line.getP2().distance(aPoint); + closestNode = nodes.get(1); + } + return closestNode; + }else{ + for(InnerPoint p : points){ + for(GraphElement ge : p.getNeighbours()) + if(ge instanceof Node){ + Node n = (Node)ge; + Direction d = new Direction(p.getBounds().getCenterX() - n.getBounds().getCenterX(),p.getBounds().getCenterY() - n.getBounds().getCenterY()); + if(n.getConnectionPoint(d).distance(aPoint) < minDist){ + minDist = n.getConnectionPoint(d).distance(aPoint); + closestNode = n; + } + } + } + return closestNode; + } + } + + //------------------------ + private void removePoint(InnerPoint p){ + /* we assume at this moment p has one neighbours only */ + InnerPoint neighbour = (InnerPoint)p.neighbours.get(0); + points.remove(p); + neighbour.neighbours.remove(p); + if(neighbour.neighbours.size() == 1) + removePoint(neighbour); + } + + /** + Draw the edge. + @param g2 the graphics context + */ + public abstract void draw(Graphics2D g2); + + /** + Tests whether the edge contains a point. + @param aPoint the point to test + @return true if this edge contains aPoint + */ + public boolean contains(Point2D aPoint){ + if(points.isEmpty()){ + return fatStrokeContains (getSegment(nodes.get(0), nodes.get(1)), aPoint); + } + for(InnerPoint p : points){ + for(GraphElement ge : p.neighbours){ + if(fatStrokeContains(getSegment(p,ge),aPoint)) + return true; + } + } + return false; + } + + private boolean fatStrokeContains(Shape s, Point2D p){ + BasicStroke fatStroke = new BasicStroke((float) (2 * MAX_DIST)); + Shape fatPath = fatStroke.createStrokedShape(s); + return fatPath.contains(p); + } + + protected Line2D.Double getSegment(GraphElement start, GraphElement end){ + Rectangle2D startBounds = start.getBounds(); + Rectangle2D endBounds = end.getBounds(); + Direction d = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); + return new Line2D.Double(start.getConnectionPoint(d), end.getConnectionPoint(d.turn(180))); + } + + public List<Point2D> getConnectionPoints(){ + List<Point2D> list = new LinkedList<Point2D>(); + if(points.isEmpty()){ + Line2D line = getSegment(nodes.get(0),nodes.get(1)); + list.add(line.getP1()); + list.add(line.getP2()); + }else{ + for(InnerPoint p : points){ + for(GraphElement ge : p.neighbours) + if(ge instanceof Node){ + Direction d = new Direction(p.getBounds().getCenterX() - ge.getBounds().getCenterX(),p.getBounds().getCenterY() - ge.getBounds().getCenterY()); + list.add(((Node)ge).getConnectionPoint(d)); + } + } + } + return list; + } + + public abstract int getStipplePattern(); + + /** + Connect this edge to the nodes. + @param aStart the starting node + @param anEnd the ending node + */ + public void connect(List<DiagramNode> nodes) throws ConnectNodesException{ + assert(getNodesNum() == 0); + /* this is to eliminate duplicates */ + LinkedHashSet<Node> nodeSet = new LinkedHashSet<Node>(); + for(DiagramNode n : nodes) + nodeSet.add((Node)n); + + /* checks on connection consistency */ + if((nodeSet == null)||(nodeSet.size() < minAttachedNodes)) + throw new ConnectNodesException("You must select at least "+ minAttachedNodes + "nodes"); + if((nodeSet.size() > maxAttachedNodes)) + throw new ConnectNodesException("You must select at most " + maxAttachedNodes +" nodes"); + + points = new ArrayList<InnerPoint>(); + if(nodeSet.size() > 2){ + /* there are more than three nodes. compute the central inner point * + * which will connect all the nodes, as the middle points of the edge bound */ + Rectangle2D bounds = new Rectangle2D.Double(); + for(Node n : nodeSet){ + bounds.add(n.getBounds()); + } + InnerPoint p = new InnerPoint(); + p.translate(new Point2D.Double(0,0), bounds.getCenterX(), bounds.getCenterY()); + p.neighbours.addAll(nodeSet); + points.add(p); + } + this.nodes.addAll(nodeSet); + + if(!points.isEmpty()) + masterInnerPoint = points.get(0); + } + + public void bend(Point2D p) { + boolean found = false; + if(points.isEmpty()){ + newInnerPoint.neighbours.addAll(nodes); + points.add(newInnerPoint); + newPointCreated = false; + }else if(newPointCreated){ + /* find the segment where the new point lays */ + for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext() && !found; ){ + InnerPoint ePoint = pItr.next(); + for(ListIterator<GraphElement> geItr = ePoint.neighbours.listIterator(); geItr.hasNext() && !found;){ + /* find the neighbour of the current edge point whose line the new point lays on */ + GraphElement ge = geItr.next(); + if(fatStrokeContains(getSegment(ePoint, ge),downPoint)){ + if(ge instanceof InnerPoint ){ + /* remove current edge point from the neighbour's neighbours */ + ((InnerPoint)ge).neighbours.remove(ePoint); + ((InnerPoint)ge).neighbours.add(newInnerPoint); + } + /*remove old neighbour from edgePoint neighbours */ + geItr.remove(); + newInnerPoint.neighbours.add(ePoint); + newInnerPoint.neighbours.add(ge); + /* add the new node to the list of EdgeNodes of this edge */ + pItr.add(newInnerPoint); + geItr.add(newInnerPoint); + found = true; + } + } + } + newPointCreated = false; + } + newInnerPoint.translate(p, p.getX() - newInnerPoint.getBounds().getCenterX(), + p.getY() - newInnerPoint.getBounds().getCenterY()); + notifyChange(new ElementChangedEvent(this,this,"bend")); + } + + /* + * this methods checks for segments of the edge which are aligned and makes a unique edge out of them + */ + public void stopMove(){ + for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){ + InnerPoint ePoint = pItr.next(); + if(ePoint.neighbours.size() > 2) + continue; + Rectangle2D startBounds = ePoint.getBounds(); + Rectangle2D endBounds = ePoint.neighbours.get(0).getBounds(); + Direction d1 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); + endBounds = ePoint.neighbours.get(1).getBounds(); + Direction d2 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); + if(d1.isParallel(d2)){ + InnerPoint p = null; + GraphElement q = null; + if(ePoint.neighbours.get(0) instanceof InnerPoint){ + p = (InnerPoint)ePoint.neighbours.get(0); + q = ePoint.neighbours.get(1); + p.neighbours.add(q); + p.neighbours.remove(ePoint); + } + if(ePoint.neighbours.get(1) instanceof InnerPoint){ + p = (InnerPoint)ePoint.neighbours.get(1); + q = ePoint.neighbours.get(0); + p.neighbours.add(q); + p.neighbours.remove(ePoint); + } + pItr.remove(); + } + } + notifyChange(new ElementChangedEvent(this,this,"stop_move")); + } + + public void startMove(Point2D downPoint){ + this.downPoint = downPoint; + newInnerPoint = null; + for(InnerPoint itrPoint : points) + if(itrPoint.contains(downPoint)){ + /* clicked on an already existing EdgePoint */ + newInnerPoint = itrPoint; + newPointCreated = false; + } + if(newInnerPoint == null){ + /* no point under the click, create a new one */ + newInnerPoint = new InnerPoint(); + newInnerPoint.translate(downPoint, downPoint.getX() - newInnerPoint.getBounds().getCenterX(), + downPoint.getY() - newInnerPoint.getBounds().getCenterY()); + newPointCreated = true; + } + } + + @Override + public void translate(Point2D p, double dx, double dy){ + for(InnerPoint ip : points) + ip.translate(p, dx, dy); + } + + public Line2D getNameLine(){ + if(points.isEmpty()){ // straight line + return getSegment(nodes.get(0),nodes.get(1)); + }else{ + if(masterInnerPoint != null){/* multiended edge */ + Rectangle2D bounds = masterInnerPoint.getBounds(); + Point2D p = new Point2D.Double(bounds.getCenterX() - 1,bounds.getCenterY()); + Point2D q = new Point2D.Double(bounds.getCenterX() + 1,bounds.getCenterY()); + return new Line2D.Double(p, q); + }else{ + GraphElement ge1 = nodes.get(0); + GraphElement ge2 = nodes.get(1); + InnerPoint c1 = null; + InnerPoint c2 = null; + + for(InnerPoint innp : points){ + if(innp.getNeighbours().contains(ge1)){ + c1 = innp; + } + if(innp.getNeighbours().contains(ge2)){ + c2 = innp; + } + } + + /* we only have two nodes but the edge has been bended */ + while((c1 != c2)&&(!c2.getNeighbours().contains(c1))){ + if(c1.getNeighbours().get(0) == ge1){ + ge1 = c1; + c1 = (InnerPoint)c1.getNeighbours().get(1); + } + else{ + ge1 = c1; + c1 = (InnerPoint)c1.getNeighbours().get(0); + } + if(c2.getNeighbours().get(0) == ge2){ + ge2 = c2; + c2 = (InnerPoint)c2.getNeighbours().get(1); + } + else{ + ge2 = c2; + c2 = (InnerPoint)c2.getNeighbours().get(0); + } + } + + Point2D p = new Point2D.Double(); + Point2D q = new Point2D.Double(); + if(c1 == c2){ + Rectangle2D bounds = c1.getBounds(); + p.setLocation( bounds.getCenterX() - 1,bounds.getCenterY()); + q.setLocation( bounds.getCenterX() + 1,bounds.getCenterY()); + }else{ + Rectangle2D bounds = c1.getBounds(); + p.setLocation( bounds.getCenterX(),bounds.getCenterY()); + bounds = c2.getBounds(); + q.setLocation(bounds.getCenterX(),bounds.getCenterY()); + + } + return new Line2D.Double(p,q); + } + } + } + + @Override + public abstract Rectangle2D getBounds(); + + public void encode(Document doc, Element parent, List<Node> nodes){ + parent.setAttribute(PersistenceManager.TYPE,getType()); + parent.setAttribute(PersistenceManager.NAME, getName()); + parent.setAttribute(PersistenceManager.ID, String.valueOf(getId())); + + int numNodes = getNodesNum(); + if(numNodes > 0){ + Element nodesTag = doc.createElement(PersistenceManager.NODES); + parent.appendChild(nodesTag); + for(int i=0; i<numNodes;i++){ + Element nodeTag = doc.createElement(PersistenceManager.NODE); + nodeTag.setAttribute(PersistenceManager.ID, String.valueOf(getNodeAt(i).getId())); + nodeTag.setAttribute(PersistenceManager.LABEL, getEndLabel(getNodeAt(i))); + nodesTag.appendChild(nodeTag); + } + } + + if(!points.isEmpty()){ + Element pointsTag = doc.createElement(PersistenceManager.POINTS); + parent.appendChild(pointsTag); + for(InnerPoint point : points){ + Element pointTag = doc.createElement(PersistenceManager.POINT); + pointsTag.appendChild(pointTag); + pointTag.setAttribute(PersistenceManager.ID, String.valueOf(-(points.indexOf(point)+1))); + + Element positionTag = doc.createElement(PersistenceManager.POSITION); + pointTag.appendChild(positionTag); + Rectangle2D bounds = point.getBounds(); + positionTag.setAttribute(PersistenceManager.X,String.valueOf(bounds.getX())); + positionTag.setAttribute(PersistenceManager.Y,String.valueOf(bounds.getY())); + + Element neighboursTag = doc.createElement(PersistenceManager.NEIGHBOURS); + pointTag.appendChild(neighboursTag); + StringBuilder builder = new StringBuilder(); + for(GraphElement ge : point.getNeighbours()){ + if(ge instanceof Node){ + builder.append(((Node)ge).getId()); + }else{ + builder.append(-(points.indexOf(ge)+1)); + } + builder.append(" "); + } + builder.deleteCharAt(builder.length()-1); + neighboursTag.setTextContent(builder.toString()); + } + } + } + + + public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{ + setName(edgeTag.getAttribute(PersistenceManager.NAME)); + if(getName().isEmpty()) + throw new IOException(); + try{ + setId(Integer.parseInt(edgeTag.getAttribute(PersistenceManager.ID))); + }catch(NumberFormatException nfe){ + throw new IOException(nfe); + } + + NodeList nodeList = edgeTag.getElementsByTagName(PersistenceManager.NODE); + List<DiagramNode> attachedNodes = new ArrayList<DiagramNode>(nodeList.getLength()); + List<String> labels = new ArrayList<String>(nodeList.getLength()); + for(int i=0; i<nodeList.getLength();i++){ + String id = ((Element)nodeList.item(i)).getAttribute(PersistenceManager.ID); + if(!nodesId.containsKey(id)) + throw new IOException(); + attachedNodes.add(nodesId.get(id)); + labels.add(((Element)nodeList.item(i)).getAttribute(PersistenceManager.LABEL)); + } + + try { + connect(attachedNodes); + } catch (ConnectNodesException e) { + throw new IOException(e); + } + + for(int i=0; i < labels.size(); i++){ + setEndLabel(attachedNodes.get(i), labels.get(i)); + } + + Map<String, InnerPoint> pointsId = new LinkedHashMap<String, InnerPoint>(); + NodeList pointTagList = edgeTag.getElementsByTagName(PersistenceManager.POINT); + + for(int i=0; i<pointTagList.getLength(); i++){ + InnerPoint point = new InnerPoint(); + Element pointTag = (Element)pointTagList.item(i); + String id = pointTag.getAttribute(PersistenceManager.ID); + /* id of property nodes must be a negative value */ + try{ + if(Integer.parseInt(id) >= 0) + throw new IOException(); + }catch(NumberFormatException nfe){ + throw new IOException(nfe); + } + + pointsId.put(id, point); + + if(pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0) == null) + throw new IOException(); + Element pointPositionTag = (Element)pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0); + double dx = 0,dy = 0; + try{ + dx = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.X)); + dy = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.Y)); + }catch(NumberFormatException nfe){ + throw new IOException(); + } + point.translate(new Point2D.Double(), dx, dy); + } + + /* remove the master inner point eventually created by connect */ + /* we're going to replace it with the one in the XML file */ + points.clear(); + /* re do the cycle when all the points id have been Map-ped */ + for(int i=0; i<pointTagList.getLength(); i++){ + Element pointTag = (Element)pointTagList.item(i); + InnerPoint point = pointsId.get(pointTag.getAttribute(PersistenceManager.ID)); + + if(pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0) == null) + throw new IOException(); + Element pointNeighboursTag = (Element)pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0); + String pointNeighboursTagContent = pointNeighboursTag.getTextContent(); + String[] neighboursId = pointNeighboursTagContent.split(" "); + + for(String neighbourId : neighboursId){ + GraphElement ge = nodesId.get(neighbourId); + if(ge == null) // it ain't a node + ge = pointsId.get(neighbourId); + if(ge == null) + throw new IOException(); + point.neighbours.add(ge); + } + points.add(point); + } + } + + @Override + public Point2D getConnectionPoint(Direction d){return null;} + + public int getMinAttachedNodes(){ + return minAttachedNodes; + } + + public int getMaxAttachedNodes(){ + return maxAttachedNodes; + } + + public LineStyle getStyle(){ + return style; + } + + protected Point2D downPoint; + private List<Node> nodes; + + /* list containing the vertex of the edge which are not nodes */ + protected List<InnerPoint> points; + protected InnerPoint newInnerPoint; + protected InnerPoint masterInnerPoint; + protected boolean newPointCreated; + private int minAttachedNodes; + private int maxAttachedNodes; + + private LineStyle style; + + + private static final double MAX_DIST = 3; + private static final Color POINT_COLOR = Color.GRAY; + public static final String NO_ARROW_STRING = ResourceBundle.getBundle(EditorFrame.class.getName()).getString("no_arrow_string"); + + + protected static class InnerPoint implements GraphElement{ + + public InnerPoint(){ + bounds = new Rectangle2D.Double(0,0,DIM,DIM); + neighbours = new LinkedList<GraphElement>(); + } + + @Override + public void startMove(Point2D p){} + + @Override + public void stopMove(){} + + @Override + public void draw(Graphics2D g2){ + Color oldColor = g2.getColor(); + g2.setColor(POINT_COLOR); + g2.fill(bounds); + g2.setColor(oldColor); + } + + @Override + public boolean contains(Point2D p){ + return bounds.contains(p); + } + + @Override + public Point2D getConnectionPoint(Direction d){ + return new Point2D.Double(bounds.getCenterX(),bounds.getCenterY()); + } + + @Override + public void translate(Point2D p, double dx, double dy){ + bounds.setFrame(bounds.getX() + dx, + bounds.getY() + dy, + bounds.getWidth(), + bounds.getHeight()); + } + + @Override + public Rectangle2D getBounds(){ + return (Rectangle2D)bounds.clone(); + } + + public List<GraphElement> getNeighbours(){ + return neighbours; + } + + public boolean hasNeighbour(GraphElement neighbour){ + return neighbours.contains(neighbour); + } + + @Override + public String toString(){ + return "EdgePoint: "+bounds.getCenterX()+"-"+bounds.getCenterY(); + } + + private Rectangle2D bounds; + private List<GraphElement> neighbours; + private static final int DIM = 5; + } + + public static class PointRepresentation { + public PointRepresentation(int size){ + xs = new double[size]; + ys = new double[size]; + adjMatrix = new BitSet[size]; + for(int i=0; i<size; i++){ + adjMatrix[i] = new BitSet(size); + } + } + public double xs[]; + public double ys[]; + public BitSet adjMatrix[]; + public int nodeStart; // the index of the beginning of the nodes in the adjMatrix + } + + public PointRepresentation getPointRepresentation(){ + PointRepresentation pr = new PointRepresentation(points.size()+nodes.size()); + if(points.isEmpty()){ // two ended edge + pr.xs[0] = nodes.get(0).getBounds().getCenterX(); + pr.ys[0] = nodes.get(0).getBounds().getCenterY(); + pr.xs[1] = nodes.get(1).getBounds().getCenterX(); + pr.ys[1] = nodes.get(1).getBounds().getCenterY(); + // we only need one edge, else it would be painted twice + pr.adjMatrix[0].set(1); + pr.nodeStart = 0; + }else{ + //[ point 1, point 2, point 3, ... , point n, node, 1 node 2, ... , node n ] + int pSize = points.size(); + pr.nodeStart = pSize; // the first node starts after the points + for(int i=0; i<pSize;i++){ + pr.xs[i] = points.get(i).getBounds().getCenterX(); + pr.ys[i] = points.get(i).getBounds().getCenterY(); + for(GraphElement ge : points.get(i).neighbours){ + if(ge instanceof InnerPoint) + pr.adjMatrix[i].set(points.indexOf(ge)); + else //Node + pr.adjMatrix[i].set(pSize+nodes.indexOf(ge)); + } + } + /* set the coordinates of the nodes, no adj matrix needed as the inner points are enough */ + for(int i=0 ; i<nodes.size(); i++){ + pr.xs[pSize+i] = nodes.get(i).getBounds().getCenterX(); + pr.ys[pSize+i] = nodes.get(i).getBounds().getCenterY(); + } + } + return pr; + } + +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EdgePopupMenu.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,172 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPopupMenu; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * A pop up menu displaying the possible operations to perform on an edge on from a visual representation + * of a diagram. + * + */ +@SuppressWarnings("serial") +public class EdgePopupMenu extends JPopupMenu { + + /** + * Creates a pop up menu, showing set name, set end label and select arrow head menu items. + * @param edge the edge that will be edited + * @param node the node whose end label and arrow head that will be edited + * @param parentComponent the component where the pop up will appear + * @param modelUpdater the model updater for applying changes + */ + public EdgePopupMenu( Edge edge, Node node, Component parentComponent, DiagramModelUpdater modelUpdater){ + this.edgeRef = edge; + this.nodeRef = node; + this.parentComponent = parentComponent; + this.modelUpdater = modelUpdater; + arrowHeads = new Object[edgeRef.getAvailableEndDescriptions().length + 1]; + for(int i=0;i<edgeRef.getAvailableEndDescriptions().length;i++){ + arrowHeads[i] = edgeRef.getAvailableEndDescriptions()[i].toString(); + } + arrowHeads[arrowHeads.length-1] = Edge.NO_ARROW_STRING; + addMenuItems(false); + } + + /** + * creates a pop up menu, showing set name menu item only + * @param edge the edge being edited + * @param parentComponent the component where the pop up will appear + * @param modelUpdater the model updater for applying changes + */ + public EdgePopupMenu( Edge edge, Component parentComponent,DiagramModelUpdater modelUpdater){ + this.edgeRef = edge; + this.parentComponent = parentComponent; + this.modelUpdater = modelUpdater; + addMenuItems(true); + } + + private void addMenuItems(boolean showSetNameMenuItemOnly){ + + JMenuItem setNameMenuItem = new JMenuItem(resources.getString("menu.set_name")); + setNameMenuItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent e) { + if(!modelUpdater.getLock(edgeRef, Lock.NAME)){ + iLog("Could not get the lock on edge for name",DiagramElement.toLogString(edgeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.name")); + return; + } + iLog("open rename edge dialog",DiagramElement.toLogString(edgeRef)); + String name = JOptionPane.showInputDialog(parentComponent, MessageFormat.format(resources.getString("dialog.input.name"),edgeRef.getName()), edgeRef.getName()); + if(name == null) + iLog("cancel rename edge dialog",DiagramElement.toLogString(edgeRef)); + else + /* edge has been locked at selection time */ + modelUpdater.setName(edgeRef,name); + modelUpdater.yieldLock(edgeRef, Lock.NAME); + } + }); + + add(setNameMenuItem); + if(!showSetNameMenuItemOnly){ + /* Label menu item */ + JMenuItem setLabelMenuItem = new JMenuItem(resources.getString("menu.set_label")); + setLabelMenuItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + if(!modelUpdater.getLock(edgeRef, Lock.EDGE_END)){ + iLog("Could not get lock on edge for end label",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.end")); + return; + } + iLog("open edge label dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + String text = JOptionPane.showInputDialog(parentComponent,resources.getString("dialog.input.label")); + if(text != null) + modelUpdater.setEndLabel(edgeRef,nodeRef,text); + else + iLog("cancel edge label dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END); + } + }); + add(setLabelMenuItem); + + if(arrowHeads.length > 1){ + /* arrow head menu item */ + JMenuItem selectArrowHeadMenuItem = new JMenuItem(resources.getString("menu.choose_arrow_head")); + selectArrowHeadMenuItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent e) { + if(!modelUpdater.getLock(edgeRef, Lock.EDGE_END)){ + iLog("Could not get lock on edge for arrow head",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.end")); + return; + } + iLog("open select arrow head dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + String arrowHead = (String)JOptionPane.showInputDialog( + parentComponent, + resources.getString("dialog.input.arrow"), + resources.getString("dialog.input.arrow.title"), + JOptionPane.PLAIN_MESSAGE, + null, + arrowHeads, + arrowHeads); + if(arrowHead == null){ + iLog("cancel select arrow head dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END); + return; + } + for(int i=0; i<edgeRef.getAvailableEndDescriptions().length;i++){ + if(edgeRef.getAvailableEndDescriptions()[i].toString().equals(arrowHead)){ + modelUpdater.setEndDescription(edgeRef, nodeRef, i); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END); + return; + } + } + /* the user selected the none menu item */ + modelUpdater.setEndDescription(edgeRef,nodeRef, Edge.NO_END_DESCRIPTION_INDEX); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END); + } + }); + add(selectArrowHeadMenuItem); + } + } + } + + private void iLog(String action, String args){ + InteractionLog.log("GRAPH",action,args); + } + + public Edge edgeRef; + private Node nodeRef; + private Component parentComponent; + private Object[] arrowHeads; + private DiagramModelUpdater modelUpdater; + private static ResourceBundle resources = ResourceBundle.getBundle(EdgePopupMenu.class.getName()); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EdgePopupMenu.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,13 @@ + + +menu.set_label=Set Label +menu.choose_arrow_head=Choose Arrow Head +menu.set_name=Set Name + +dialog.lock_failure.end=Edge end is being edited by another user +dialog.lock_failure.name=Edge name is being edited by another user + +dialog.input.label=Enter Label Text +dialog.input.arrow=Choose Arrow Head +dialog.input.arrow.title=Select +dialog.input.name=Renaming {0}, Enter new name \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,1951 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.KeyboardFocusManager; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JTree; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.EdgeReferenceMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeReferenceMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.PropertyMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.PropertyTypeMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.TypeMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; +import uk.ac.qmul.eecs.ccmi.haptics.Haptics; +import uk.ac.qmul.eecs.ccmi.haptics.HapticsFactory; +import uk.ac.qmul.eecs.ccmi.network.ClientConnectionManager; +import uk.ac.qmul.eecs.ccmi.network.Command; +import uk.ac.qmul.eecs.ccmi.network.DiagramDownloader; +import uk.ac.qmul.eecs.ccmi.network.DiagramShareException; +import uk.ac.qmul.eecs.ccmi.network.NetDiagram; +import uk.ac.qmul.eecs.ccmi.network.ProtocolFactory; +import uk.ac.qmul.eecs.ccmi.network.Server; +import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.Narrator; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.ExceptionHandler; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; +import uk.ac.qmul.eecs.ccmi.utils.Validator; + +/** +* The main frame of the editor which contains diagram panes that show graphs and tree representations of diagrams. +*/ +@SuppressWarnings("serial") +public class EditorFrame extends JFrame { +/** + Constructs a blank frame with a desktop pane + but no graph windows. + @param appClassName the fully qualified app class name. + It is expected that the resources are appClassName + "Strings" + and appClassName + "Version" (the latter for version-specific + resources) + */ + public EditorFrame(Haptics haptics, File[] templateFiles, String backupDirPath, TemplateEditor[] templateEditors){ + this.backupDirPath = backupDirPath; + /* load resources */ + resources = ResourceBundle.getBundle(this.getClass().getName()); + + /* haptics */ + this.haptics = haptics; + hapticTrigger = new HapticTrigger(); + + /* read editor related preferences */ + preferences = PreferencesService.getInstance(); + + setIconImage(new ResourceFactory.ImageFactory().getImage("ccmi_favicon.gif")); + String laf = preferences.get("laf", null); + if (laf != null) changeLookAndFeel(laf); + + recentFiles = new ArrayList<String>(); + File lastDir = new File("."); + String recent = preferences.get("recent", "").trim(); + if (recent.length() > 0){ + recentFiles.addAll(Arrays.asList(recent.split("[|]"))); + lastDir = new File(recentFiles.get(0)).getParentFile(); + } + fileService = new FileService.ChooserService(lastDir); + + /* set up extensions */ + defaultExtension = resources.getString("files.extension"); + extensionFilter = new ExtensionFilter( + resources.getString("files.name"), + new String[] { defaultExtension }); + exportFilter = new ExtensionFilter( + resources.getString("files.image.name"), + resources.getString("files.image.extension")); + + /* start building the GUI */ + editorTabbedPane = new EditorTabbedPane(this); + setContentPane(editorTabbedPane); + + + setTitle(resources.getString("app.name")); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + + int screenWidth = (int)screenSize.getWidth(); + int screenHeight = (int)screenSize.getHeight(); + + setLocation(screenWidth / 16, screenHeight / 16); + editorTabbedPane.setPreferredSize(new Dimension( + screenWidth * 5 / 8, screenHeight * 5 / 8)); + + /* install the player listener (a narrator speech) for CANCEL, EMPTY and MESSAGE ok event. They are * + * not depending on what the user is doing and the speech is therefore always the same. We only need * + * to set the listener once and it will do the job each time the sound is triggered */ + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + DiagramPanel dPanel = getActiveTab(); + if(dPanel != null){// we can cancel a dialog even when no diagram is open (e.g. for tcp connections) + speakFocusedComponent(""); + }else{ + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.cancelled"),"")); + } + } + }, SoundEvent.CANCEL); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + DiagramPanel dPanel = getActiveTab(); + DiagramTree tree = dPanel.getTree(); + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.empty_property"),tree.currentPathSpeech())); + } + }, SoundEvent.EMPTY); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + DiagramPanel dPanel = getActiveTab(); + DiagramTree tree = dPanel.getTree(); + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }, SoundEvent.MESSAGE_OK); + + /* setup listeners */ + initListeners(); + /* set up menus */ + this.templateEditors = templateEditors; + existingTemplateNames = new ArrayList<String>(10); + existingTemplates = new ArrayList<Diagram>(10); + int extensionLength = resources.getString("template.extension").length(); + for(File file : templateFiles){ + existingTemplateNames.add(file.getName().substring(0, file.getName().length()-extensionLength)); + } + initMenu(); + /* read template files. this call must be placed after menu creation as it adds menu items to file->new-> */ + boolean someTemplateFilesNotRead = readTemplateFiles(templateFiles); + /* become visible */ + pack(); + setVisible(true); + /* if some templates were not read successfully, warn the user with a message */ + if(someTemplateFilesNotRead){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.error.filesnotread"), SpeechOptionPane.WARNING_MESSAGE); + } + } + + private void initListeners(){ + /* window closing */ + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter(){ + @Override + public void windowClosing(WindowEvent event){ + exit(); + } + @Override + public void windowOpened(WindowEvent e) { + // bring the window to front, else the openGL window would have higher priority + e.getWindow().toFront(); + } + }); + + addWindowFocusListener(new WindowFocusListener(){ + @Override + public void windowGainedFocus(WindowEvent evt) { + if(evt.getOppositeWindow() == null) + NarratorFactory.getInstance().speak(resources.getString("window.focus")); + } + + @Override + public void windowLostFocus(WindowEvent evt) { + if(evt.getOppositeWindow() == null) + NarratorFactory.getInstance().speak(resources.getString("window.unfocus")); + } + }); + + /* set up listeners reacting to change of selection in the tree and tab */ + tabChangeListener = new ChangeListener(){ + @Override + public void stateChanged(ChangeEvent evt) { + DiagramPanel diagramPanel = getActiveTab(); + if (diagramPanel != null){ + /* give the focus to the Content Pane, else it's grabbed by the rootPane */ + getContentPane().requestFocusInWindow(); + TreePath path = diagramPanel.getTree().getSelectionPath(); + treeEnabledMenuUpdate(path); + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("window.tab"),editorTabbedPane.getTitleAt(editorTabbedPane.getSelectedIndex()))); + + // updated the haptics + int newTabId = editorTabbedPane.getSelectedIndex(); + HapticsFactory.getInstance().switchDiagram(newTabId); + iLog("diagram tab changed to "+editorTabbedPane.getTitleAt(editorTabbedPane.getSelectedIndex())); + }else{ + treeEnabledMenuUpdate(null); + } + /* if we change tab, the haptic highlight must be set again */ + selectHapticHighligh(null); + /* so do the menu depending on the panel */ + diagramPanelEnabledMenuUpdate(diagramPanel); + } + }; + editorTabbedPane.addChangeListener(tabChangeListener); + + treeSelectionListener = new TreeSelectionListener(){ + @Override + public void valueChanged(TreeSelectionEvent evt) { + treeEnabledMenuUpdate(evt.getPath()); + } + }; + + netLocalDiagramExceptionHandler = new ExceptionHandler(){ + @Override + public void handleException(Exception e) { + SwingUtilities.invokeLater(new Runnable(){ + @Override + public void run() { + SpeechOptionPane.showMessageDialog(EditorFrame.this, resources.getString("dialog.error.local_server")); + } + }); + } + }; + } + + private void initMenu(){ + ResourceFactory factory = new ResourceFactory(resources); + + JMenuBar menuBar = factory.createMenuBar(); + setJMenuBar(menuBar); + + /* --- FILE MENU --- */ + JMenu fileMenu = factory.createMenu("file"); + menuBar.add(fileMenu); + + /* menu items and listener added by addDiagramType function */ + newMenu = factory.createMenu("file.new"); + fileMenu.add(newMenu); + + JMenuItem fileOpenItem = factory.createMenuItem( + "file.open", this, "openFile"); + fileMenu.add(fileOpenItem); + + recentFilesMenu = factory.createMenu("file.recent"); + buildRecentFilesMenu(); + fileMenu.add(recentFilesMenu); + + fileSaveItem = factory.createMenuItem("file.save", this, "saveFile"); + fileMenu.add(fileSaveItem); + + fileSaveAsItem = factory.createMenuItem("file.save_as", this, "saveFileAs"); + fileMenu.add(fileSaveAsItem); + + fileCloseItem = factory.createMenuItem("file.close",this,"closeFile"); + fileMenu.add(fileCloseItem); + + graphExportItem = factory.createMenuItem("file.export_image", this, "exportImage"); + fileMenu.add(graphExportItem); + + JMenuItem fileExitItem = factory.createMenuItem( + "file.exit", this, "exit"); + fileMenu.add(fileExitItem); + + JMenu editMenu = factory.createMenu("edit"); + menuBar.add(editMenu); + + locateMenuItem = factory.createMenuItem("edit.locate", this,"locate"); + editMenu.add(locateMenuItem); + + highlightMenuItem = factory.createMenuItem("edit.highlight", this, "highlight"); + highlightMenuItem.setEnabled(false); + editMenu.add(highlightMenuItem); + + jumpMenuItem = factory.createMenuItem("edit.jump",this,"jump"); + editMenu.add(jumpMenuItem); + + insertMenuItem = factory.createMenuItem("edit.insert", this, "insert"); + editMenu.add(insertMenuItem); + + deleteMenuItem = factory.createMenuItem("edit.delete",this,"delete"); + editMenu.add(deleteMenuItem); + + renameMenuItem = factory.createMenuItem("edit.rename",this,"rename"); + editMenu.add(renameMenuItem); + + bookmarkMenuItem = factory.createMenuItem("edit.bookmark",this,"editBookmarks"); + editMenu.add(bookmarkMenuItem); + + editNotesMenuItem = factory.createMenuItem("edit.edit_note",this,"editNotes"); + editMenu.add(editNotesMenuItem); + + /* --- VIEW MENU --- */ + JMenu viewMenu = factory.createMenu("view"); + menuBar.add(viewMenu); + + viewMenu.add(factory.createMenuItem( + "view.zoom_out", new + ActionListener(){ + public void actionPerformed(ActionEvent event){ + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + dPanel.getGraphPanel().changeZoom(-1); + } + })); + + viewMenu.add(factory.createMenuItem( + "view.zoom_in", new + ActionListener(){ + public void actionPerformed(ActionEvent event){ + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + dPanel.getGraphPanel().changeZoom(1); + } + })); + + viewMenu.add(factory.createMenuItem( + "view.grow_drawing_area", new + ActionListener(){ + public void actionPerformed(ActionEvent event){ + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + GraphPanel gPanel = dPanel.getGraphPanel(); + Rectangle2D bounds = gPanel.getGraphBounds(); + bounds.add(gPanel.getBounds()); + gPanel.setMinBounds(new Rectangle2D.Double(0, 0, + GROW_SCALE_FACTOR * bounds.getWidth(), + GROW_SCALE_FACTOR * bounds.getHeight())); + gPanel.revalidate(); + gPanel.repaint(); + } + })); + + viewMenu.add(factory.createMenuItem( + "view.clip_drawing_area", new + ActionListener(){ + public void actionPerformed(ActionEvent event){ + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + GraphPanel gPanel = dPanel.getGraphPanel(); + gPanel.setMinBounds(null); + gPanel.revalidate(); + gPanel.repaint(); + } + })); + + viewMenu.add(factory.createMenuItem( + "view.smaller_grid", new + ActionListener() + { + public void actionPerformed(ActionEvent event) + { + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + dPanel.getGraphPanel().changeGridSize(-1); + } + })); + + viewMenu.add(factory.createMenuItem( + "view.larger_grid", new + ActionListener(){ + public void actionPerformed(ActionEvent event){ + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + dPanel.getGraphPanel().changeGridSize(1); + } + })); + + final JCheckBoxMenuItem hideGridItem; + viewMenu.add(hideGridItem = (JCheckBoxMenuItem) factory.createCheckBoxMenuItem( + "view.hide_grid", new + ActionListener() + { + public void actionPerformed(ActionEvent event) + { + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + JCheckBoxMenuItem menuItem = (JCheckBoxMenuItem) event.getSource(); + dPanel.getGraphPanel().setHideGrid(menuItem.isSelected()); + } + })); + + viewMenu.addMenuListener(new + MenuListener(){ + /* changes the checkbox according to the diagram selected */ + public void menuSelected(MenuEvent event){ + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + hideGridItem.setSelected(dPanel.getGraphPanel().getHideGrid()); + } + public void menuDeselected(MenuEvent event){} + public void menuCanceled(MenuEvent event){} + }); + + JMenu lafMenu = factory.createMenu("view.change_laf"); + viewMenu.add(lafMenu); + + UIManager.LookAndFeelInfo[] infos = + UIManager.getInstalledLookAndFeels(); + for (int i = 0; i < infos.length; i++){ + final UIManager.LookAndFeelInfo info = infos[i]; + JMenuItem item = SpeechMenuFactory.getMenuItem(info.getName()); + lafMenu.add(item); + item.addActionListener(new + ActionListener(){ + public void actionPerformed(ActionEvent event){ + String laf = info.getClassName(); + changeLookAndFeel(laf); + preferences.put("laf", laf); + } + }); + } + + /* --- TEMPLATE --- */ + JMenu templateMenu = factory.createMenu("template"); + menuBar.add(templateMenu); + + for(final TemplateEditor templateEditor : templateEditors){ + templateMenu.add(factory.createMenuItemFromLabel( + templateEditor.getLabelForNew(), + new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + Diagram diagram = templateEditor.createNew(EditorFrame.this, existingTemplateNames); + if(diagram == null) + return; + try{ + saveDiagramTemplate(diagram); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog( + EditorFrame.this, + resources.getString("dialog.error.save_template")); + return; + } + addDiagramType(diagram); + SpeechOptionPane.showMessageDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.template_created"), diagram.getName()), + SpeechOptionPane.INFORMATION_MESSAGE); + } + }) + ); + + templateMenu.add(factory.createMenuItemFromLabel( + templateEditor.getLabelForEdit(), + new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt){ + if(existingTemplates.isEmpty()){ + NarratorFactory.getInstance().speak(resources.getString("dialog.error.no_template_to_edit")); + return; + } + Diagram[] diagrams = new Diagram[existingTemplates.size()]; + diagrams = existingTemplates.toArray(diagrams); + Diagram selectedDiagram = (Diagram)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + resources.getString("dialog.input.edit_diagram_template"), + diagrams, + diagrams[0]); + + if(selectedDiagram == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return; + } + + Diagram diagram = templateEditor.edit(EditorFrame.this, existingTemplateNames,selectedDiagram); + if(diagram == null) + return; + try{ + saveDiagramTemplate(diagram); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog( + EditorFrame.this, + resources.getString("dialog.error.save_template")); + return; + } + addDiagramType(diagram); + SpeechOptionPane.showMessageDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.template_created"), diagram.getName()), + SpeechOptionPane.INFORMATION_MESSAGE); + } + } + )); + } + + /* --- SOUND --- */ + JMenu soundMenu = factory.createMenu("sound"); + menuBar.add(soundMenu); + + JMenuItem muteMenuItem = factory.createCheckBoxMenuItem("sound.mute", new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + JCheckBoxMenuItem menuItem = (JCheckBoxMenuItem) evt.getSource(); + NarratorFactory.getInstance().setMuted(menuItem.isSelected()); + SoundFactory.getInstance().setMuted(menuItem.isSelected()); + } + }); + soundMenu.add(muteMenuItem); + + JMenuItem rateMenuItem = factory.createMenuItem("sound.rate", new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + Integer newRate = SpeechOptionPane.showNarratorRateDialog( + EditorFrame.this, + resources.getString("dialog.input.sound_rate"), + NarratorFactory.getInstance().getRate(), + Narrator.MIN_RATE, + Narrator.MAX_RATE); + if(newRate != null){ + NarratorFactory.getInstance().setRate(newRate); + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString("dialog.speech_rate.message"), + newRate)); + }else{ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + } + }); + soundMenu.add(rateMenuItem); + + /* --- COLLABORATION ---- */ + JMenu collabMenu = factory.createMenu("collab"); + menuBar.add(collabMenu); + + startServer = factory.createMenuItem("collab.start_server", this, "startServer"); + collabMenu.add(startServer); + + stopServer = factory.createMenuItem("collab.stop_server", this, "stopServer"); + collabMenu.add(stopServer); + stopServer.setEnabled(false); + + shareDiagramMenuItem = factory.createMenuItem("collab.share_diagram", this, "shareDiagram"); + collabMenu.add(shareDiagramMenuItem); + + collabMenu.add(factory.createMenuItem("collab.open_shared_diagram", this, "openSharedDiagram")); + + /* --- HELP --- */ + JMenu helpMenu = factory.createMenu("help"); + menuBar.add(helpMenu); + + helpMenu.add(factory.createMenuItem( + "help.about", this, "showAboutDialog")); + + helpMenu.add(factory.createMenuItem( + "help.license", this, "showLicense")); + + treeEnabledMenuUpdate(null); + diagramPanelEnabledMenuUpdate(null); + } + + private void changeLookAndFeel(String lafName){ + try{ + UIManager.setLookAndFeel(lafName); + SwingUtilities.updateComponentTreeUI(EditorFrame.this); + } + catch (ClassNotFoundException ex) {} + catch (InstantiationException ex) {} + catch (IllegalAccessException ex) {} + catch (UnsupportedLookAndFeelException ex) {} + } + + /** + * Adds a file name to the "recent files" list and rebuilds the "recent files" menu. + * @param newFile the file name to add + */ + private void addRecentFile(final String newFile){ + recentFiles.remove(newFile); + if (newFile == null || newFile.equals("")) return; + recentFiles.add(0, newFile); + buildRecentFilesMenu(); + } + + /* speaks out the selected tree node if the tree is focused or the tab label if the tab is focused */ + private void speakFocusedComponent(String message){ + message = (message == null) ? "" : message+"; ";//add a dot to pause the TTS + + DiagramPanel dPanel = getActiveTab(); + if(dPanel != null){ + String focusedComponent = null; + if(KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() instanceof JTree) + focusedComponent = dPanel.getTree().currentPathSpeech(); + else + focusedComponent = MessageFormat.format(resources.getString("window.tab"),editorTabbedPane.getComponentTabTitle(dPanel)); + NarratorFactory.getInstance().speak(message+focusedComponent); + } + } + /** + * Rebuilds the "recent files" menu. + */ + private void buildRecentFilesMenu(){ + recentFilesMenu.removeAll(); + for (int i = 0; i < recentFiles.size(); i++){ + final String file = recentFiles.get(i); + String name = new File(file).getName(); + JMenuItem item = SpeechMenuFactory.getMenuItem(name); + item.setToolTipText(file); + recentFilesMenu.add(item); + item.addActionListener(new + ActionListener(){ + public void actionPerformed(ActionEvent event){ + try { + FileService.Open open = new FileService.DirectService().open(new File(((JMenuItem)event.getSource()).getToolTipText())); + InputStream in = open.getInputStream(); + String path = open.getPath(); + for(int i=0; i<editorTabbedPane.getTabCount();i++){ + if(path.equals(editorTabbedPane.getToolTipTextAt(i))){ + editorTabbedPane.setSelectedIndex(i); + return; + } + } + Diagram diagram = PersistenceManager.decodeDiagramInstance(in); + addTab(open.getPath(), diagram); + } catch (IOException exception) { + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + exception.getLocalizedMessage()); + } + } + }); + } + } + + /** + Asks the user to open a graph file. + */ + public void openFile(){ + InputStream in = null; + try{ + FileService.Open open = fileService.open(null,null, extensionFilter,this); + in = open.getInputStream(); + if(in != null){ // open.getInputStream() == null -> user clicked on cancel + String path = open.getPath(); + int index = editorTabbedPane.getPathTabIndex(path); + if(index != -1){ //diagram is already open + editorTabbedPane.setSelectedIndex(index); + speakFocusedComponent(""); + return; + } + /* every opened diagram must have a unique name */ + if(editorTabbedPane.getDiagramNameTabIndex(open.getName()) != -1) + throw new IOException(resources.getString("dialog.error.same_file_name")); + iLog("START READ LOCAL DIAGRAM "+open.getName()); + Diagram diagram = PersistenceManager.decodeDiagramInstance(in); + iLog("END READ LOCAL DIAGRAM "+open.getName()); + /* force the name of the diagram to be the same as the file name * + * it should never be useful, unless the .ccmi file is edited manually */ + diagram.setName(open.getName()); + addTab(open.getPath(), diagram); + addRecentFile(open.getPath()); + } + } + catch (IOException exception) { + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + exception.getLocalizedMessage()); + }finally{ + if(in != null) + try{in.close();}catch(IOException ioe){ioe.printStackTrace();} + + } + } + + public void closeFile(){ + DiagramPanel dPanel = getActiveTab(); + if(dPanel.isModified()||dPanel.getFilePath() == null){ + int answer = SpeechOptionPane.showConfirmDialog( + EditorFrame.this, + resources.getString("dialog.confirm.close"), + SpeechOptionPane.YES_NO_OPTION); + + if(answer == SpeechOptionPane.YES_OPTION) // save file only if the user decides to + if(!saveFile()) + return; /* if the user closes the save dialog do nothing */ + } + iLog("diagram closed :"+dPanel.getDiagram().getName()); + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.diagram_closed"),dPanel.getDiagram().getName())); + editorTabbedPane.remove(dPanel); + } + + public boolean saveFile(){ + DiagramPanel diagramPanel = getActiveTab(); + if (diagramPanel == null) // no tabs open + return false; + String fileName = diagramPanel.getFilePath(); + if (fileName == null) { + return saveFileAs(); + } + + OutputStream out = null; + try{ + File file = new File(fileName); + out = new BufferedOutputStream(new FileOutputStream(file)); + Diagram d = diagramPanel.getDiagram(); + PersistenceManager.encodeDiagramInstance(d, out); + /* we saved the diagram, therefore there are no more pending changes */ + diagramPanel.setModified(false); + speakFocusedComponent(resources.getString("dialog.file_saved")); + return true; + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(editorTabbedPane,ioe.getLocalizedMessage()); + return false; + }finally{ + try { + out.close(); + }catch(IOException ioe){ /*can't do anything */ } + } + } + + /** + Saves the current graph as a new file. + */ + public boolean saveFileAs() { + DiagramPanel diagramPanel = getActiveTab(); + if (diagramPanel == null) // no tabs open + return false; + OutputStream out = null; + try { + FileService.Save save = fileService.save( + PreferencesService.getInstance().get("dir.diagrams", "."), + diagramPanel.getFilePath(), + extensionFilter, + null, + defaultExtension); + out = save.getOutputStream(); + if (out == null) /* user didn't select any file for saving */ + return false; + + String fileName = save.getName(); + /* there cannot be two diagrams with the same name open at the same time */ + if(editorTabbedPane.getDiagramNameTabIndex(save.getName()) != -1 && !fileName.equals(diagramPanel.getName())) + throw new IOException(resources.getString("dialog.error.same_file_name")); + + PersistenceManager.encodeDiagramInstance(diagramPanel.getDiagram(),save.getName(), out); + /* update the diagram panel, after the saving */ + diagramPanel.setFilePath(save.getPath()); + diagramPanel.setModified(false); + speakFocusedComponent(resources.getString("dialog.file_saved")); + return true; + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(editorTabbedPane,ioe.getLocalizedMessage()); + return false; + }finally{ + if(out != null) + try{out.close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + + /** + Exits the program if no graphs have been modified + or if the user agrees to abandon modified graphs. + */ + public void exit(){ + /* check first whether there are modified diagrams */ + int diagramsToSave = 0; + for(int i=0; i<editorTabbedPane.getTabCount();i++){ + DiagramPanel dPanel = editorTabbedPane.getComponentAt(i); + if(dPanel.isModified()||dPanel.getFilePath() == null){ + diagramsToSave++; + } + } + + if(diagramsToSave > 0){ + int answer = SpeechOptionPane.showConfirmDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.confirm.exit"), diagramsToSave), + SpeechOptionPane.YES_NO_OPTION); + // if the doesn't want to save changes, veto the close + if(answer != SpeechOptionPane.NO_OPTION){ + if(answer == SpeechOptionPane.YES_OPTION){ // user clicked on yes button we just get them back to the editor + speakFocusedComponent(""); + }else{// user pressed the ESC button + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + return; + } + } + + NarratorFactory.getInstance().dispose(); + SoundFactory.getInstance().dispose(); + haptics.dispose(); + if(server != null) + server.shutdown(); + if(clientConnectionManager != null) + clientConnectionManager.shutdown(); + while(haptics.isAlive()){/* wait */} + /* closes the logger's handlers */ + iLog("PROGRAM EXIT"); + InteractionLog.dispose(); + + savePreferences(); + System.exit(0); + } + + public void jump(){ + String[] options = new String[canJumpRef ? 4 : 3]; + options[0] = resources.getString("options.jump.type"); + options[1] = resources.getString("options.jump.diagram"); + options[2] = resources.getString("options.jump.bookmark"); + if(canJumpRef){ + options[3] = resources.getString("options.jump.reference"); + } + iLog("open jump to dialog",""); + String result = (String)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + resources.getString("dialog.input.jump.select"), + options, + options[0]); + DiagramPanel dPanel = getActiveTab(); + DiagramTree tree = dPanel.getTree(); + if(result != null){ + if(result.equals(options[0])){ // jump type + tree.jump(DiagramTree.JumpTo.SELECTED_TYPE); + }else if(result.equals(options[1])){// diagram + tree.jump(DiagramTree.JumpTo.ROOT); + }else if(result.equals(options[2])){// bookmark + tree.jump(DiagramTree.JumpTo.BOOKMARK); + }else if(result.equals(options[3])){ + tree.jump(DiagramTree.JumpTo.REFERENCE); + } + }else{ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel jump to dialog",""); + } + } + + public void locate(){ + DiagramPanel dPanel = getActiveTab(); + DiagramTree tree = dPanel.getTree(); + DiagramElement de = (DiagramElement)tree.getSelectionPath().getLastPathComponent(); + HapticsFactory.getInstance().attractTo(System.identityHashCode(de)); + iLog("locate " +((de instanceof Node)? "node" : "edge"),DiagramElement.toLogString(de)); + } + + public void highlight() { + getActiveTab().getTree().jumpTo(hapticHighlightDiagramElement); + iLog("highlight " +((hapticHighlightDiagramElement instanceof Node)? "node" : "edge"),DiagramElement.toLogString(hapticHighlightDiagramElement)); + } + + public void insert(){ + DiagramPanel dPanel = getActiveTab(); + final DiagramTree tree = dPanel.getTree(); + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)tree.getSelectionPath().getLastPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + if(treeNode instanceof TypeMutableTreeNode){ //adding a diagram Element + TypeMutableTreeNode typeNode = (TypeMutableTreeNode)treeNode; + final DiagramElement diagramElement = (DiagramElement)typeNode.getPrototype().clone(); + try { + if(diagramElement instanceof Edge){ + Edge edge = (Edge)diagramElement; + edge.connect(Arrays.asList(tree.getSelectedNodes())); + iLog("insert edge",DiagramElement.toLogString(edge)); + modelUpdater.insertInTree(diagramElement); + + final StringBuilder builder = new StringBuilder(); + for(int i=0; i<edge.getNodesNum();i++){ + if(i == edge.getNodesNum()-1) + builder.append(edge.getNodeAt(i)+resources.getString("speech.input.edge.ack")); + else + builder.append(edge.getNodeAt(i)+ resources.getString("speech.input.edge.ack2")); + } + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(builder.toString()); + } + }, SoundEvent.OK); + SoundFactory.getInstance().play(SoundEvent.OK); + /* remove the selections on the edge's nodes and release their lock */ + tree.clearNodeSelections(); + for(int i=0; i<edge.getNodesNum();i++){ + modelUpdater.yieldLock(edge, Lock.MUST_EXIST); + } + }else{ // adding a Node + iLog("insert node ",DiagramElement.toLogString(diagramElement)); + modelUpdater.insertInTree(diagramElement); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.input.node.ack"),diagramElement.spokenText())); + } + }, SoundEvent.OK); + SoundFactory.getInstance().play(SoundEvent.OK); + } + } catch (ConnectNodesException cne) { + final String message = cne.getLocalizedMessage(); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(message); + } + }, SoundEvent.ERROR); + SoundFactory.getInstance().play(SoundEvent.ERROR); + iLog("insert edge error",message); + } + }else if(treeNode instanceof PropertyTypeMutableTreeNode){ //adding a property + PropertyTypeMutableTreeNode propTypeNode = (PropertyTypeMutableTreeNode)treeNode; + Node n = (Node)propTypeNode.getNode(); + if(!modelUpdater.getLock(n, Lock.PROPERTIES)){ + iLog("Could not get lock on node for add properties",DiagramElement.toLogString(n)); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.properties"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + + iLog("open insert property dialog",""); + final String propertyValue = SpeechOptionPane.showInputDialog(EditorFrame.this, + MessageFormat.format(resources.getString("dialog.input.property.text"),propTypeNode.getName()) + ); + if(propertyValue != null){ + if(!propertyValue.isEmpty()){ //check that the user didn't enter an empty string + iLog("insert property ", propTypeNode.getType()+" "+propertyValue); + modelUpdater.addProperty(n, propTypeNode.getType(), propertyValue); + SoundFactory.getInstance().play(SoundEvent.OK,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.input.property.ack"),propertyValue)); + } + }); + }else{ + SoundFactory.getInstance().play(SoundEvent.EMPTY); + iLog("insert property", ""); + } + }else{ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel insert property dialog",""); + } + modelUpdater.yieldLock(n, Lock.PROPERTIES); + }else if(treeNode instanceof PropertyMutableTreeNode){ // edit modifiers + iLog("open modifiers dialog",""); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)treeNode.getParent(); + Node n = (Node)typeNode.getNode(); + Modifiers modifiers = n.getProperties().getModifiers(typeNode.getType()); + if(modifiers.isNull()){ + iLog("error:no modifiers for this property",""); + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("dialog.warning.null_modifiers"),typeNode.getType())); + }else{ + if(!modelUpdater.getLock(n, Lock.PROPERTIES)){ + iLog("Could not get lock on node for set modifiers",DiagramElement.toLogString(n)); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.properties"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + + int index = typeNode.getIndex(treeNode); + Set<Integer> result = SpeechOptionPane.showModifiersDialog(EditorFrame.this, + MessageFormat.format(resources.getString("dialog.input.check_modifiers"),n.getProperties().getValues(typeNode.getType()).get(index)) , + modifiers.getTypes(), + modifiers.getIndexes(index) + ); + if(result == null){ + iLog("cancel modifiers dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + }else{ + iLog("edit modifiers",Arrays.toString(result.toArray())); + modelUpdater.setModifiers(n, typeNode.getType(), index, result); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }, SoundEvent.OK); + SoundFactory.getInstance().play(SoundEvent.OK); + } + modelUpdater.yieldLock(n, Lock.PROPERTIES); + } + }else{ //NodeReferenceMutableTreeNode = edit label and arrow head + NodeReferenceMutableTreeNode nodeRef = (NodeReferenceMutableTreeNode)treeNode; + Node n = (Node)nodeRef.getNode(); + Edge e = (Edge)nodeRef.getEdge(); + if(!modelUpdater.getLock(e, Lock.EDGE_END)){ + iLog("Could not get lock on edge for end label",DiagramElement.toLogString(e)); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.end_label"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + iLog("open edge operation selection dialog",""); + + boolean hasAvailArrowHeads = (e.getAvailableEndDescriptions().length > 0); + String[] operations = new String[hasAvailArrowHeads ? 2 : 1]; + operations[0] = resources.getString("dialog.input.edge_operation.label"); + if(hasAvailArrowHeads) + operations[1] = resources.getString("dialog.input.edge_operation.arrow_head"); + String choice = (String)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + resources.getString("dialog.input.edge_operation.select"), + operations, + operations[0]); + + if(choice == null){ + iLog("cancel edge operation selection dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + modelUpdater.yieldLock(e, Lock.EDGE_END); + return; + } + if(choice.equals(operations[0])){ //operations[0] = edit edge end-label + iLog("open edge label dialog",""); + String label = SpeechOptionPane.showInputDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.input.edge_label"),n.getType(), n.getName()), + e.getEndLabel(n) ); + if(label != null){ + modelUpdater.setEndLabel(e, n, label); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }, SoundEvent.OK); + SoundFactory.getInstance().play(SoundEvent.OK); + }else{ + iLog("cancel edge label dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + }else{//operations[1] = edit edge arrow head + String[] endDescriptions = new String[e.getAvailableEndDescriptions().length+1]; + for(int i=0;i<e.getAvailableEndDescriptions().length;i++) + endDescriptions[i] = e.getAvailableEndDescriptions()[i]; + endDescriptions[endDescriptions.length-1] = Edge.NO_ARROW_STRING; + + iLog("open edge arrow head dialog",""); + final String endDescription = (String)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.input.edge_arrowhead"),n.getType(), n.getName()), + endDescriptions, + endDescriptions[0] + ); + if(endDescription != null){ + int index = Edge.NO_END_DESCRIPTION_INDEX; + for(int i=0;i<e.getAvailableEndDescriptions().length;i++) + if(endDescription.equals(e.getAvailableEndDescriptions()[i])){ + index = i; + break; + } + modelUpdater.setEndDescription(e, n, index); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }, SoundEvent.OK); + SoundFactory.getInstance().play(SoundEvent.OK); + }else{ + iLog("cancel edge arrow head dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + } + modelUpdater.yieldLock(e, Lock.EDGE_END); + } + } + + public void delete(){ + DiagramPanel dPanel = getActiveTab(); + final DiagramTree tree = dPanel.getTree(); + final DiagramModelTreeNode treeNode = (DiagramModelTreeNode)tree.getSelectionPath().getLastPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + if(treeNode instanceof DiagramElement){ //delete a diagram element + final DiagramElement element = (DiagramElement)treeNode; + if(!modelUpdater.getLock(element, Lock.DELETE)){ + iLog("Could not get lock on element for deletion",DiagramElement.toLogString(element)); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.delete"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + iLog("open delete "+ ((element instanceof Node)? "node" : "edge") +" dialog",""); + int choice = SpeechOptionPane.showConfirmDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.confirm.deletion"),element.getType(), element.getName()), + SpeechOptionPane.OK_CANCEL_OPTION); + if(choice != SpeechOptionPane.OK_OPTION){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel delete " + ((element instanceof Node)? "node" : "edge") +" dialog",""); + modelUpdater.yieldLock(element, Lock.DELETE); + return; + } + modelUpdater.takeOutFromTree(element); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.delete.element.ack"),element.spokenText(),tree.currentPathSpeech())); + } + }, SoundEvent.OK); + SoundFactory.getInstance().play(SoundEvent.OK); + }else if(treeNode.getParent() instanceof PropertyTypeMutableTreeNode){ //deleting a property + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)treeNode.getParent(); + Node n = (Node)typeNode.getNode(); + if(!modelUpdater.getLock(n, Lock.PROPERTIES)){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.properties"), SpeechOptionPane.INFORMATION_MESSAGE); + iLog("Could not get lock for properties for deletion",DiagramElement.toLogString(n)); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + iLog("open delete property dialog",""); + int choice = SpeechOptionPane.showConfirmDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.confirm.deletion"),typeNode.getType(),treeNode.getName()), + SpeechOptionPane.OK_CANCEL_OPTION); + if(choice != SpeechOptionPane.OK_OPTION){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel delete property dialog",""); + modelUpdater.yieldLock(n, Lock.PROPERTIES); + return; + } + modelUpdater.removeProperty(n, typeNode.getType(), typeNode.getIndex(treeNode)); + SoundFactory.getInstance().play(SoundEvent.OK,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.deleted.property.ack"),treeNode.spokenText(),tree.currentPathSpeech())); + } + }); + }else + throw new IllegalStateException("Cannot delete a tree node of type: "+treeNode.getClass().getName()); + } + + public void rename(){ + DiagramPanel dPanel = getActiveTab(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + final DiagramTree tree = dPanel.getTree(); + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)tree.getSelectionPath().getLastPathComponent(); + MessageFormat formatter = new MessageFormat(resources.getString("dialog.input.rename")); + if(treeNode instanceof DiagramElement){ + DiagramElement element = (DiagramElement)dPanel.getTree().getSelectionPath().getLastPathComponent(); + Object arg[] = {element.getName()}; + if(!modelUpdater.getLock(element, Lock.NAME)){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.name"),SpeechOptionPane.INFORMATION_MESSAGE); + iLog("Could not get lock on element for renaming",DiagramElement.toLogString(element)); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + iLog("open rename "+((element instanceof Node)? "node" : "edge")+" dialog",DiagramElement.toLogString(element)); + String name = SpeechOptionPane.showInputDialog(EditorFrame.this, + formatter.format(arg), + element.getName()); + if(name != null){ + modelUpdater.setName(element,name); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }, SoundEvent.OK); + SoundFactory.getInstance().play(SoundEvent.OK); + }else{ + iLog("cancel rename "+((element instanceof Node)? "node" : "edge")+" dialog",DiagramElement.toLogString(element)); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + modelUpdater.yieldLock(element, Lock.NAME); + }else if(treeNode instanceof PropertyMutableTreeNode){ + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)treeNode.getParent(); + Node n = (Node)typeNode.getNode(); + if(!modelUpdater.getLock(n, Lock.PROPERTIES)){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.properties"), SpeechOptionPane.INFORMATION_MESSAGE); + iLog("Could not get lock on properties for renaming",DiagramElement.toLogString(n)); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + Object arg[] = {treeNode.getName()}; + iLog("open rename property dialog",treeNode.getName()); + String name = SpeechOptionPane.showInputDialog(EditorFrame.this, + formatter.format(arg), + treeNode.getName()); + if(name == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel rename property dialog",treeNode.getName()); + return; + } + modelUpdater.setProperty(n, typeNode.getType(), typeNode.getIndex(treeNode), name); + SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }, SoundEvent.OK); + SoundFactory.getInstance().play(SoundEvent.OK); + modelUpdater.yieldLock(n, Lock.PROPERTIES); + }else + throw new IllegalStateException("Cannot delete a tree node of type: "+treeNode.getClass().getName()); + } + + public void editBookmarks(){ + boolean addBookmark = true; + DiagramPanel dPanel = getActiveTab(); + final DiagramTree tree = dPanel.getTree(); + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)tree.getLastSelectedPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + + if(!modelUpdater.getLock(treeNode, Lock.BOOKMARK)){ + iLog("Cannot get lock on tree node for bookmark", treeNode.getName()); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.bookmark"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + + if(!treeNode.getBookmarkKeys().isEmpty()){ + /* the are already bookmarks, thus we let the user chose whether they want to */ + /* add a new one or remove an old one */ + String[] options = { + resources.getString("dialog.input.bookmark.select.add"), + resources.getString("dialog.input.bookmark.select.remove") + }; + + iLog("open select add/remove bookmark dialog",""); + String result = (String)SpeechOptionPane.showSelectionDialog(EditorFrame.this, + resources.getString("dialog.input.bookmark.select.add_remove"), + options, + options[0]); + + if(result == null){ + iLog("cancel select add/remove bookmark dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + modelUpdater.yieldLock(treeNode, Lock.BOOKMARK); + return; + } + if(result.equals(options[1])) + addBookmark = false; + } + + if(addBookmark){ + boolean uniqueBookmarkChosen = false; + while(!uniqueBookmarkChosen){ + iLog("open add bookmark dialog",""); + String bookmark = SpeechOptionPane.showInputDialog(EditorFrame.this, resources.getString("dialog.input.bookmark.text")); + if(bookmark != null){ + if("".equals(bookmark)){ + iLog("error: entered empty bookmark",""); + SoundFactory.getInstance().play(SoundEvent.ERROR, new PlayerListener(){ + @Override + public void playEnded(){ + NarratorFactory.getInstance().speak(resources.getString("dialog.input.bookmark.text.empty")); + } + }); + }else if(tree.getModel().getBookmarks().contains(bookmark)){ + iLog("error: entered bookmark already existing",bookmark); + SoundFactory.getInstance().play(SoundEvent.ERROR,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(resources.getString("dialog.input.bookmark.text.already_existing")); + } + }); + }else{ + tree.getModel().putBookmark(bookmark, treeNode); + uniqueBookmarkChosen = true; + SoundFactory.getInstance().play(SoundEvent.OK,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }); + } + }else{ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel add bookmark dialog",""); + break; //user no longer wants to choose, exit the dialog thus + } + } + }else{ // removing a bookmark + String[] bookmarksArray = new String[treeNode.getBookmarkKeys().size()]; + bookmarksArray = treeNode.getBookmarkKeys().toArray(bookmarksArray); + + iLog("open remove bookmark dialog",""); + final String bookmark = (String)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + resources.getString("dialog.input.bookmark.delete"), + bookmarksArray, + bookmarksArray[0] + ); + + if(bookmark != null){ + tree.getModel().removeBookmark(bookmark); + SoundFactory.getInstance().play(SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.delete.bookmark.ack"), bookmark,tree.currentPathSpeech())); + } + }); + }else{ + iLog("cancel remove bookmark dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + } + modelUpdater.yieldLock(treeNode, Lock.BOOKMARK); + } + + public void editNotes(){ + DiagramPanel dPanel = getActiveTab(); + final DiagramTree tree = dPanel.getTree(); + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)dPanel.getTree().getLastSelectedPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + if(!modelUpdater.getLock(treeNode, Lock.NOTES)){ + iLog("Could not get lock on tree node for notes",treeNode.getName()); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.notes"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + + String typeString = ""; + /* if the note is for a diagram element the dialog message is changed so that the type precedes the name */ + if(treeNode instanceof DiagramElement){ + typeString = ((DiagramElement)treeNode).getType() + " "; + } + /* if the note is for a property tree node the dialog message is changed so that the type precedes the name */ + if(treeNode instanceof PropertyMutableTreeNode){ + PropertyTypeMutableTreeNode parent = (PropertyTypeMutableTreeNode)treeNode.getParent(); + typeString = parent.getType() + " "; + } + iLog("open edit note dialog",""); + String result = SpeechOptionPane.showTextAreaDialog(EditorFrame.this, resources.getString("dialog.input.notes.text")+typeString+treeNode.getName() ,treeNode.getNotes()); + if(result != null){ + modelUpdater.setNotes(treeNode, result); + SoundFactory.getInstance().play(SoundEvent.OK,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }); + }else{ + iLog("cancel edit note dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + modelUpdater.yieldLock(treeNode, Lock.NOTES); + } + + public void startServer(){ + iLog("server started",""); + server = Server.createServer(); + try{ + server.init(EditorFrame.this); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + ioe.getLocalizedMessage()); + iLog("error: starting server",ioe.getLocalizedMessage()); + return; + } + server.start(); + startServer.setEnabled(false); + stopServer.setEnabled(true); + if(getActiveTab() != null) + shareDiagramMenuItem.setEnabled(true); + } + + public void stopServer(){ + /* those network diagrams which are connected to the local server are reverted, * + * that is the diagram panel is set with the delegate diagram of the network diagram */ + for(int i=0; i < editorTabbedPane.getTabCount(); i++){ + DiagramPanel dPanel = editorTabbedPane.getComponentAt(i); + if(dPanel.getDiagram() instanceof NetDiagram){ + NetDiagram netDiagram = (NetDiagram)dPanel.getDiagram(); + if(netDiagram.getSocketChannel().equals(localSocket)) + dPanel.setDiagram(netDiagram.getDelegate()); + } + } + server.shutdown(resources.getString("server.shutdown_msg")); + server = null; + if(localSocket != null){ + try{localSocket.close();}catch(IOException ioe){ioe.printStackTrace();} + localSocket = null; + } + startServer.setEnabled(true); + stopServer.setEnabled(false); + shareDiagramMenuItem.setEnabled(false); + iLog("server stopped",""); + } + + public void shareDiagram(){ + try{ + if(server == null) + throw new DiagramShareException(resources.getString("server.not_running_exc")); + + DiagramPanel dPanel = getActiveTab(); + Diagram diagram = dPanel.getDiagram(); + try { + iLog("share diagram",diagram.getName()); + /* check if it's already connected to the local server (a.k.a. another diagram has been shared previously */ + if(localSocket == null){ + int port = Integer.parseInt(PreferencesService.getInstance().get("server.remote_port",Server.DEFAULT_REMOTE_PORT)); + InetSocketAddress address = new InetSocketAddress("127.0.0.1",port); + localSocket = SocketChannel.open(address); + } + + server.share(diagram); + ProtocolFactory.newInstance().send(localSocket, new Command(Command.Name.LOCAL,diagram.getName())); + dPanel.setDiagram(NetDiagram.wrapLocalHost(diagram,localSocket,server.getLocalhostQueue(diagram.getName()),netLocalDiagramExceptionHandler)); + shareDiagramMenuItem.setEnabled(false); + } catch (IOException e) { + iLog("error sharing diagram",diagram.getName()+" "+e.getLocalizedMessage()); + SpeechOptionPane.showMessageDialog(EditorFrame.this, e.getLocalizedMessage()); + return; + } + }catch(DiagramShareException dse){ + SpeechOptionPane.showMessageDialog(EditorFrame.this, dse.getLocalizedMessage()); + } + } + + public void openSharedDiagram(){ + iLog("open open share diagram dialog",""); + /* open the window prompting for the server address and make checks on the user input */ + String addr = SpeechOptionPane.showInputDialog(EditorFrame.this, "Enter server address"); + if(addr == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel open share diagram dialog",""); + return; + }else if(!Validator.validateIPAddr(addr)){ + iLog("error:invalid IP address",addr); + SpeechOptionPane.showMessageDialog(this, resources.getString("speech.invalid_ip")); + return; + }else{ + /* open the channel for the new diagram */ + SocketChannel channel = null; + try { + channel = SocketChannel.open(); + } catch (IOException e) { + iLog("error:could not connect to the server",""); + SpeechOptionPane.showMessageDialog(EditorFrame.this, "Could not connect to server"); + return; + } + /* download the diagram list */ + DiagramDownloader downloader = new DiagramDownloader( + channel, + addr, + DiagramDownloader.Task.CONNECT_AND_DOWNLOAD_LIST + ); + iLog("open download diagram list dialog",""); + int option = SpeechOptionPane.showProgressDialog(EditorFrame.this, "Downloading diagram list", downloader,500); + if(option == SpeechOptionPane.CANCEL_OPTION){ + iLog("cancel download diagram list dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();} + }else{ + try{ + /* show the available diagram list */ + String result = downloader.get(); + if(result == null) + throw new Exception(resources.getString("dialog.error.no_diagrams_on_server")); // go to the catch block + String[] diagramsList = result.split("\n"); + + iLog("open select diagram to download dialog",""); + String diagramName = (String)SpeechOptionPane.showSelectionDialog(EditorFrame.this, "Select diagram to download", diagramsList, diagramsList[0]); + if(diagramName == null){ + iLog("cancel select diagram to download dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();} + return; + } + /* there cannot be two diagrams with the same name open at the same time */ + if(editorTabbedPane.getDiagramNameTabIndex(diagramName) != -1) + throw new IOException(resources.getString("dialog.error.same_file_name")); + /* download the chosen diagram */ + downloader = new DiagramDownloader(channel,diagramName,DiagramDownloader.Task.DOWNLOAD_DIAGRAM); + iLog("open downloading diagram dialog",diagramName); + option = SpeechOptionPane.showProgressDialog(EditorFrame.this, MessageFormat.format(resources.getString("dialog.downloading_diagram"), diagramName), downloader,500); + if(option == SpeechOptionPane.CANCEL_OPTION){ + iLog("cancel downloading diagram dialog",diagramName); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}; + }else{ + result = downloader.get(); + + if(clientConnectionManager == null){ + clientConnectionManager = new ClientConnectionManager(editorTabbedPane); + clientConnectionManager.start(); + }else if(!clientConnectionManager.isAlive()){ + clientConnectionManager = new ClientConnectionManager(editorTabbedPane); + clientConnectionManager.start(); + } + + iLog("START READ NETWORK DIAGRAM "+diagramName); + Diagram diagram = PersistenceManager.decodeDiagramInstance(new BufferedInputStream(new ByteArrayInputStream(result.getBytes("UTF-8")))); + iLog("END READ NETWORK DIAGRAM "+diagramName); + /* remove all the bookmarks in the server diagram model instance */ + for(String bookmarkKey : diagram.getTreeModel().getBookmarks()) + diagram.getTreeModel().removeBookmark(bookmarkKey); + Diagram newDiagram = NetDiagram.wrapRemoteHost(diagram,clientConnectionManager,channel); + addTab(null,newDiagram); + clientConnectionManager.addRequest(new ClientConnectionManager.AddDiagramRequest(channel, diagram)); + } + }catch(RuntimeException rte){ + throw new RuntimeException(rte); + }catch(ExecutionException ee){ + /* if the exception happened in the DiagramDownloader then it's wrapped into an * + * ExecutionException and we have to unwrap it to get a neat message for the user */ + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + ee.getCause().getLocalizedMessage()); + iLog("error: "+ee.getCause().getMessage(),""); + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}; + }catch(Exception exception){ + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + exception.getLocalizedMessage()); + iLog("error: "+exception.getMessage(),""); + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}; + } + } + } + } + + public void backupOpenDiagrams(){ + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE_d_MMM_yyyy_HH_mm_ss"); + String date = dateFormat.format(new Date()); + File backupDir = new File(new StringBuilder(backupDirPath) + .append(System.getProperty("file.separator")) + .append(date) + .toString()); + backupDir.mkdir(); + for(int i=0; i<editorTabbedPane.getTabCount();i++){ + DiagramPanel dPanel = editorTabbedPane.getComponentAt(i); + if(dPanel.isModified()||dPanel.getFilePath() == null){ + Diagram diagram = dPanel.getDiagram(); + File file = new File(backupDir,diagram.getName()+".ccmi"); + try { + FileService.Save save = new FileService.DirectService().save((file)); + PersistenceManager.encodeDiagramInstance(diagram, save.getOutputStream()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + Exports the current graph to an image file. + */ + public void exportImage(){ + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + OutputStream out = null; + try{ + String imageExtensions = resources.getString("files.image.extension"); + /* default save dir is the same as the diagram's or home/images otherwise */ + String path = dPanel.getFilePath(); + if(path == null) + path = PreferencesService.getInstance().get("dir.images", "."); + FileService.Save save = fileService.save(path, dPanel.getDiagram().getName(), exportFilter, + defaultExtension, imageExtensions); + out = save.getOutputStream(); + if (out != null){ + /* if the diagram has a name (has already been saved) then prompt the user with the name of + * the diagram with a jpg extension. */ + String fileName = FileService.getFileNameFromPath(save.getPath(),true); + String extension = fileName.substring(fileName.lastIndexOf(".") + 1); + if (!ImageIO.getImageWritersByFormatName(extension).hasNext()){ + throw new IOException(MessageFormat.format( + resources.getString("dialog.error.unsupported_image"), + extension + )); + } + GraphPanel gPanel = dPanel.getGraphPanel(); + try{ + saveImage(gPanel, out, extension); + speakFocusedComponent(resources.getString("dialog.file_saved")); + }catch(IOException ioe){ + throw new IOException(resources.getString("dialog.error.save_image"),ioe); + } + } + } + catch (IOException ioe){ + SpeechOptionPane.showMessageDialog(editorTabbedPane,ioe.getMessage()); + }finally{ + if(out != null) + try{out.close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + + /** + Exports a current graph to an image file. + @param graph the graph + @param out the output stream + @param format the image file format + */ + public static void saveImage(GraphPanel graph, OutputStream out, String format) + throws IOException { + // need a dummy image to get a Graphics to measure the size + Rectangle2D bounds = graph.getBounds(); + BufferedImage image + = new BufferedImage((int)bounds.getWidth() + 1, + (int)bounds.getHeight() + 1, + BufferedImage.TYPE_INT_RGB); + Graphics2D g2 = (Graphics2D)image.getGraphics(); + g2.translate(-bounds.getX(), -bounds.getY()); + g2.setColor(Color.WHITE); + g2.fill(new Rectangle2D.Double( + bounds.getX(), + bounds.getY(), + bounds.getWidth() + 1, + bounds.getHeight() + 1)); + g2.setColor(Color.BLACK); + g2.setBackground(Color.WHITE); + boolean hideGrid = graph.getHideGrid(); + graph.setHideGrid(true); + graph.paintComponent(g2); + graph.setHideGrid(hideGrid); + ImageIO.write(image, format, out); + } + + /** + Displays the About dialog box. + */ + public void showAboutDialog(){ + String options[] = {resources.getString("dialog.ok_button")}; + SpeechSummaryPane.showDialog(this, + resources.getString("dialog.about.title"), + MessageFormat.format(resources.getString("dialog.about"), + resources.getString("app.name"), + resources.getString("app.version"), + resources.getString("dialog.about.description"), + resources.getString("dialog.about.license")), + SpeechSummaryPane.OK_OPTION, + options + ); + } + + public void showLicense() { + BufferedReader reader = null; + try{ + reader = new BufferedReader( + new InputStreamReader( + getClass().getResourceAsStream( + "license.txt"))); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null){ + builder.append(line).append('\n'); + } + String options[] = {resources.getString("dialog.ok_button")}; + SpeechSummaryPane.showDialog(editorTabbedPane, + resources.getString("dialog.license.title"), + builder.toString(), + SpeechSummaryPane.OK_OPTION,options); + }catch (IOException exception){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.error.license_not_found")); + }finally{ + if(reader != null) + try{reader.close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + + public void saveDiagramTemplate(Diagram diagram) throws IOException { + File file = new File( + new StringBuilder(PreferencesService.getInstance().get("home", ".")) + .append(System.getProperty("file.separator")) + .append(resources.getString("dir.templates")) + .append(System.getProperty("file.separator")) + .append(diagram.getName()) + .append(resources.getString("template.extension")) + .toString() + ); + PersistenceManager.encodeDiagramTemplate(diagram,file); + + } + + /** + Adds a graph type to the File->New menu. + @param resourceName the name of the menu item resource + @param graphClass the class object for the graph + */ + public void addDiagramType(final Diagram diagram){ + /* this is to prevent the user from creating other diagram prototypes with the same name */ + existingTemplateNames.add(diagram.getName()); + existingTemplates.add(diagram); + newMenu.add(new ResourceFactory(resources).configure(SpeechMenuFactory.getMenuItem(diagram.getName()),"", + new ActionListener(){ + @Override + public void actionPerformed(ActionEvent event){ + Diagram clone = (Diagram)diagram.clone(); + /* find a good unique name for the new tab */ + Pattern pattern = Pattern.compile("new "+clone.getName()+"( \\(([0-9]+)\\))?"); + int maxOpenDiagram = -1; + for(int i=0;i<editorTabbedPane.getTabCount();i++){ + Matcher matcher = pattern.matcher(editorTabbedPane.getComponentAt(i).getDiagram().getName()); + if(matcher.matches()){ + if(matcher.group(1) == null) + maxOpenDiagram = 0; + else + maxOpenDiagram = Math.max(maxOpenDiagram, Integer.parseInt(matcher.group(2))); + } + } + if(maxOpenDiagram >= 0) + clone.setName(String.format("new %s (%d)", clone.getName(),++maxOpenDiagram)); + else clone.setName("new "+clone.getName()); + addTab(null, clone); + iLog("new diagram created of type: "+diagram.getName()); + } + })); + } + + /** + * Saves the user preferences before exiting. + */ + public void savePreferences(){ + String recent = ""; + for (int i = 0; i < Math.min(recentFiles.size(), maxRecentFiles); i++){ + if (recent.length() > 0) recent += "|"; + recent += recentFiles.get(i); + } + preferences.put("recent", recent); + } + + public DiagramPanel getActiveTab(){ + return (DiagramPanel)editorTabbedPane.getSelectedComponent(); + } + + public void selectHapticHighligh(DiagramElement de){ + hapticHighlightDiagramElement = de; + highlightMenuItem.setEnabled(de == null ? false : true); + } + + private void addTab(String path, Diagram diagram){ + int newTabId = editorTabbedPane.getTabCount(); + DiagramPanel diagramPanel = new DiagramPanel(diagram,editorTabbedPane); + diagramPanel.setFilePath(path); + diagramPanel.getTree().addTreeSelectionListener(treeSelectionListener); + /* update the haptics */ + Haptics haptics = HapticsFactory.getInstance(); + haptics.addNewDiagram(newTabId, true); + for(Node n : diagram.getCollectionModel().getNodes()) + haptics.addNode(n.getBounds().getCenterX(), n.getBounds().getCenterY(), System.identityHashCode(n)); + for(Edge e : diagram.getCollectionModel().getEdges()){ + Edge.PointRepresentation pr = e.getPointRepresentation(); + HapticsFactory.getInstance().addEdge(System.identityHashCode(e),pr.xs,pr.ys,pr.adjMatrix,pr.nodeStart,e.getStipplePattern(),e.getNameLine()); + } + /* install the listener that manages the haptics device */ + diagram.getCollectionModel().addCollectionListener(hapticTrigger); + + editorTabbedPane.add(diagramPanel); + editorTabbedPane.setToolTipTextAt(newTabId,path); + editorTabbedPane.setSelectedIndex(newTabId); + /* give the focus to the Content Pane, else it's grabbed by the rootPane + and it does not work when adding a new tab with the tree focused */ + getContentPane().requestFocusInWindow(); + } + + private void diagramPanelEnabledMenuUpdate(DiagramPanel dPanel){ + fileSaveItem.setEnabled(false); + fileSaveAsItem.setEnabled(false); + fileCloseItem.setEnabled(false); + shareDiagramMenuItem.setEnabled(false); + graphExportItem.setEnabled(false); + if(dPanel == null) + return; + + fileSaveItem.setEnabled(true); + fileSaveAsItem.setEnabled(true); + graphExportItem.setEnabled(true); + if(server != null && !(dPanel.getDiagram() instanceof NetDiagram)) + shareDiagramMenuItem.setEnabled(true); + fileCloseItem.setEnabled(true); + + } + + private void treeEnabledMenuUpdate(TreePath path){ + canJumpRef = false; + insertMenuItem.setEnabled(false); + deleteMenuItem.setEnabled(false); + renameMenuItem.setEnabled(false); + editNotesMenuItem.setEnabled(false); + bookmarkMenuItem.setEnabled(false); + jumpMenuItem.setEnabled(false); + locateMenuItem.setEnabled(false); + if(path == null) + return; + + jumpMenuItem.setEnabled(true); + editNotesMenuItem.setEnabled(true); + bookmarkMenuItem.setEnabled(true); + + /* jump to reference : a reference node must be selected */ + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + + /* root node */ + if((treeNode).getParent() == null) + return; + + if(treeNode instanceof EdgeReferenceMutableTreeNode) + canJumpRef = true; + + if(treeNode instanceof NodeReferenceMutableTreeNode){ + insertMenuItem.setEnabled(true); + canJumpRef = true ; + } + + /* insert a node : the type node must be selected */ + if(treeNode instanceof TypeMutableTreeNode){ + insertMenuItem.setEnabled(true); + } + + /* it's a property node */ + if(treeNode instanceof PropertyMutableTreeNode){ + deleteMenuItem.setEnabled(true); + renameMenuItem.setEnabled(true); + insertMenuItem.setEnabled(true); + } + + if(treeNode instanceof PropertyTypeMutableTreeNode) + insertMenuItem.setEnabled(true); + if(treeNode instanceof DiagramElement){ + deleteMenuItem.setEnabled(true); + renameMenuItem.setEnabled(true); + if(HapticsFactory.getInstance().isAlive()) + locateMenuItem.setEnabled(true); + } + } + + private boolean readTemplateFiles(File[] files){ + boolean someFilesNotRead = false; + for(File file : files){ + try { + Diagram d = PersistenceManager.decodeDiagramTemplate(file); + addDiagramType(d); + } catch (IOException e) { + someFilesNotRead = true; + e.printStackTrace(); + } + } + return someFilesNotRead; + } + + private void iLog(String action,String args){ + InteractionLog.log("TREE",action,args); + } + + private void iLog(String message){ + InteractionLog.log(message); + } + + private Server server; + private SocketChannel localSocket; + private ExceptionHandler netLocalDiagramExceptionHandler; + private ClientConnectionManager clientConnectionManager; + private Haptics haptics; + private ResourceBundle resources; + private EditorTabbedPane editorTabbedPane; + private FileService.ChooserService fileService; + private PreferencesService preferences; + private HapticTrigger hapticTrigger; + private DiagramElement hapticHighlightDiagramElement; + private TemplateEditor[] templateEditors; + private ArrayList<String> existingTemplateNames; + private ArrayList<Diagram> existingTemplates; + + private JMenu newMenu; + private JMenuItem jumpMenuItem; + private boolean canJumpRef; + private JMenuItem fileSaveItem; + private JMenuItem graphExportItem; + private JMenuItem fileSaveAsItem; + private JMenuItem fileCloseItem; + private JMenuItem insertMenuItem; + private JMenuItem deleteMenuItem; + private JMenuItem renameMenuItem; + private JMenuItem bookmarkMenuItem; + private JMenuItem editNotesMenuItem; + private JMenuItem locateMenuItem; + private JMenuItem highlightMenuItem; + private JMenuItem shareDiagramMenuItem; + private JMenuItem startServer; + private JMenuItem stopServer; + private TreeSelectionListener treeSelectionListener; + private ChangeListener tabChangeListener; + private String defaultExtension; + private String backupDirPath; + private ArrayList<String> recentFiles; + private JMenu recentFilesMenu; + private int maxRecentFiles = DEFAULT_MAX_RECENT_FILES; + + private ExtensionFilter extensionFilter; + private ExtensionFilter exportFilter; + + private static final int DEFAULT_MAX_RECENT_FILES = 5; + private static final double GROW_SCALE_FACTOR = Math.sqrt(2); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,259 @@ +### APPLICATION ### +app.name=CCmI Editor +app.copyright=Copyright (C) 2002...2005 Cay S. Horstmann (http://horstmann.com) +app.version=0.1a +files.name=CCmI Diagram files +files.extension=.ccmi +template.extension=.xml +dir.home=ccmi_editor +dir.templates=templates + +### EDITOR ### + +error.version=You need Java version {0} +files.image.name=Image Files +files.image.extension=.jpg +grabber.text=Select + +window.focus=Editor window focused +window.unfocus=Editor window unfocused +window.tab=Tab, {0} + +#### DIALOGS ### +dialog.about={0} version {1}\u000A\u000A{2}\u000A\u000A{3} +dialog.about.description=Collaborative Cross-modal Interfaces\u000A \ +The Collaborative Cross-modal Interfaces (CCmI) project is a Research\u000A \ +Councils UK Digital Economy Programme funded project that aims to explore\u000A \ +the use of multi-modal input and output technologies (audio, haptics, graphics)\u000A \ +to improve the accessibility of collaboration using diagrams in the workplace.\u000A \ +The challenge is to design support for collaboration where participants have\u000A \ +differing access to modalities - we refer to these situations\u000A as cross-modal collaboration. +dialog.about.license=This program comes with ABSOLUTELY NO WARRANTY.\u000AThis is free software, and you are welcome to redistribute it\u000Aunder certain conditions.\ +Select License in the Help menu for details. +dialog.about.title=About +dialog.license.title=License + +dialog.ok_button=Ok +dialog.cancel_button=Cancel + +dialog.overwrite=Another file with the same name already exists. Do you want to overwrite it ? +dialog.properties=Properties +dialog.error.title=Error +dialog.error.filesnotread=Error: One or more templates files could not be read properly +dialog.error.local_server=Error: Problems in communication with local server +dialog.error.save_template=Error: could not save template +dialog.error.no_template_to_edit=Error: there are no template to edit +dialog.error.same_file_name=Cannot open two diagrams with the same name +dialog.error.no_diagrams_on_server=No diagrams available on the server +dialog.error.file_exists=File already exists +dialog.error.license_not_found=Could not retrieve the license +dialog.error.save_image=Error: could not save the image to file +dialog.error.unsupported_image=Error: {0} not supported + +dialog.template_created=Diagram {0} created +dialog.file_saved=File Saved +dialog.downloading_diagram=Downloading diagram: {0} + +dialog.warning.title=Warning +dialog.warning.null_modifiers=Nothing to edit for {0} + +dialog.input.bookmark.select.add_remove=What would you like to do ? +dialog.input.bookmark.select.add=Add Bookmark +dialog.input.bookmark.select.remove=Remove Bookmark +dialog.input.bookmark.text=New bookmark. Enter name +dialog.input.bookmark.text.already_existing=The chosen bookmark already exists +dialog.input.bookmark.text.empty=Bookmarks cannot be empty +dialog.input.bookmark.select.bookmark=Select bookmark +dialog.input.bookmark.select.notfound=No bookmarks available +dialog.input.bookmark.delete=Select bookmark to remove +dialog.input.bookmark.title=Bookmark +dialog.input.notes.title=Notes +dialog.input.notes.text=Edit notes for +dialog.input.property.text=New {0}, enter name +dialog.input.rename=Renaming {0}, Enter new name. +dialog.input.jump.select=Where would you like to jump to ? +dialog.input.edge_operation.label=Add label +dialog.input.edge_operation.arrow_head=Add Arrow Head +dialog.input.edge_operation.title= Edge End Operation +dialog.input.edge_operation.select=What would you like to do ? +dialog.input.edge_label=Add label to {0} {1}, enter name +dialog.input.selected_type.select=Select which type to jump to +dialog.input.check_modifiers=Editing {0}. +dialog.input.edge_arrowhead=Select arrow head for {0} {1} +dialog.input.sound_rate=Select rate value +dialog.input.edit_diagram_template=Select Diagram to edit + +dialog.confirm.deletion=Are you sure you want to delete the {0} {1} ? +dialog.confirm.deletions=Are you sure you want to delete the selected objects ? +dialog.confirm.exit={0} Unsaved Diagrams\u000ADo you want to save changes? +dialog.confirm.close=Unsaved diagram.\u000ADo you want to save changes? +dialog.confirm.title=Confirm + +dialog.lock_failure.delete=Object is being edited by another user +dialog.lock_failure.deletes=Objects are being edited by other users +dialog.lock_failure.deletes_warning=The following objects will not be deleted as they're locked by other users: +dialog.lock_failure.name=Object name is being edited by another user +dialog.lock_failure.properties=Node properties are being edited by another user +dialog.lock_failure.end_label=Edge is being edited by another user +dialog.lock_failure.arrow_head=Edge arrow heads are being edited by another user +dialog.lock_failure.move=Object is being moved by another user +dialog.lock_failure.notes=Tree node is being edited by another user +dialog.lock_failure.bookmark=Tree node is being edited by another user +dialog.lock_failure.must_exist=Element is candidate for deletion by another user + +dialog.property_editor.title=Property Editor +dialog.property_editor.error.property_null=Properties cannot be null +dialog.property_editor.edit_modifiers_button=Edit Modifiers + +dialog.modifier_editor.title=Modifier Editor + +dialog.speech_option_pane.download=Download +dialog.speech_option_pane.input=Input +dialog.speech_option_pane.select=Select +dialog.speech_option_pane.confirm=Confirm +dialog.speech_option_pane.modifiers=Select Modifiers +dialog.speech_option_pane.message= {0}. Press OK to confirm +dialog.speech_option_pane.cancel=Cancel + +dialog.file_chooser.file_type=File Type: +dialog.file_chooser.file_name=file Name: + +dialog.speech_rate.message=Speech rate set to {0} + +server.shutdown_msg=request by user +server.not_running_exc=Server not running +#### OPTIONS #### +options.jump.reference=Reference +options.jump.type=Type +options.jump.diagram=Diagram +options.jump.bookmark=Bookmark +#### SPEECH #### +speech.cancelled=Cancelled, {0} +speech.delete.element.ack={0} deleted, {1} +speech.deleted.property.ack={0} deleted, {1} +speech.delete.bookmark.ack= bookmark {0} deleted, {1} +speech.no_bookmarks=No bookmarks available +speech.note.updated=Note updated +speech.empty_property=Empty string, nothing created. {0} +speech.empty_label=Empty label +speech.selected=selected +speech.unselected=unselected +speech.input.property.ack={0} created, +speech.input.node.ack={0} , created, +speech.input.edge.ack=, connected, +speech.input.edge.ack2= and +speech.invalid_ip=invalid IP address + +speech.diagram_closed= {0} closed. +speech.node_selected={0} selected +speech.node_unselected={0} unselected +speech.jump=Jump to {0} + +### TABBED PANE ### +tab.new_tab=new {0} +tab.new_tab_id=new {0} ({1}) + +### MENU ### +file.text=File +file.mnemonic=F +file.new.text=New Diagram +file.new.mnemonic=N +file.open.text=Open... +file.open.mnemonic=O +file.open.accelerator=ctrl O +file.recent.text=Recent files +file.recent.mnemonic=R +file.save.text=Save +file.save.mnemonic=S +file.save.accelerator=ctrl S +file.save_as.text=Save as... +#file.save_as.mnemonic=A +file.close.text=Close +file.export_image.text=Export image +file.export_image.mnemonic=E +file.print.text=Print +file.print.mnemonic=P +file.exit.text=Exit +file.exit.mnemonic=X +file.exit.accelerator=ctrl X +edit.text=Edit +edit.mnemonic=E +edit.jump.text=Jump to +edit.jump.mnemonic=J +edit.jump.accelerator=ctrl J +edit.properties.text=Properties +edit.properties.mnemonic=P +edit.delete.text=Delete +edit.delete.mnemonic=D +edit.delete.accelerator=ctrl DELETE +edit.insert.text=Insert/Edit +edit.insert.mnemonic=i +edit.insert.accelerator=ctrl ENTER +edit.rename.text=Rename +edit.rename.mnemonic=R +edit.rename.accelerator=ctrl R +edit.bookmark.text=Add/Remove Bookmark +edit.bookmark.mnemonic=B +edit.bookmark.accelerator=ctrl B +edit.edit_note.text=Edit Note +edit.edit_note.mnemonic=N +edit.edit_note.accelerator=ctrl N +edit.select_next.text=Select Next +edit.select_next.mnemonic=N +edit.select_next.accelerator=ctrl RIGHT +edit.select_previous.text=Select Previous +edit.select_previous.mnemonic=P +edit.select_previous.accelerator=ctrl LEFT +edit.edit.text=Edit +edit.edit.mnemonics=U +edit.edit.accelerator=ctrl U +edit.locate.text=Find +edit.locate.mnemonics=F +edit.locate.accelerator=ctrl F +edit.highlight.text=Highlight +edit.highlight.mnemonics=H +edit.highlight.accelerator=ctrl H +sound.text=Sound +sound.mnemonic=S +sound.mute.text=Mute +sound.mute.mnemonic=M +sound.mute.accelerator=ctrl M +sound.rate.text=Set Speech Rate +sound.rate.mnemonic=R +view.text=View +view.mnemonic=V +view.zoom_out.text=Zoom out +view.zoom_out.mnemonic=O +view.zoom_out.accelerator=ctrl MINUS +view.zoom_in.text=Zoom in +view.zoom_in.mnemonic=I +view.zoom_in.accelerator=ctrl EQUALS +view.smaller_grid.text=Smaller grid +view.smaller_grid.mnemonic=S +view.grow_drawing_area.text=Grow drawing area +view.grow_drawing_area.mnemonic=G +view.clip_drawing_area.text=Clip drawing area +view.clip_drawing_area.mnemonic=C +view.larger_grid.text=Larger grid +view.larger_grid.mnemonic=L +view.hide_grid.text=Hide grid +view.hide_grid.mnemonic=H +view.change_laf.text=Change Look&Feel +view.change_laf.mnemonic=K +template.text=Template +template.mnemonic=T +collab.text=Collaboration +collab.mnemonic=C +collab.start_server.text=Start Server +collab.stop_server.text=Stop Server +collab.share_diagram.text=Share Diagram +collab.open_shared_diagram.text= Open Shared Diagram +help.text=Help +help.mnemonic=H +help.about.text=About +#help.about.mnemonic=A +help.license.text=License +help.license.mnemonic=L + + +no_arrow_string=None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorTabbedPane.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,136 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.AWTKeyStroke; +import java.awt.Component; +import java.awt.FocusTraversalPolicy; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JTabbedPane; +import javax.swing.JTree; +import javax.swing.KeyStroke; + +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * + * The tabbed pane of the editor. On each tab a {@link DiagramPanel} is displayed. + * + */ +@SuppressWarnings("serial") +public class EditorTabbedPane extends JTabbedPane { + public EditorTabbedPane(JFrame frame){ + this.frame = frame; + setFocusTraversalKeysEnabled(false); + /* get the look and feel default keys for moving the focus on (usually = TAB) */ + for(AWTKeyStroke keyStroke : KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)) + getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(keyStroke.getKeyCode(),keyStroke.getModifiers()),"tab"); + + /* add action to the moving focus keys: reproduce focus system and add speech to it */ + getActionMap().put("tab", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FocusTraversalPolicy policy = KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalPolicy(); + Component next = policy.getComponentAfter(EditorTabbedPane.this.frame, KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); + next.requestFocusInWindow(); + String componentType = (next instanceof JTree) ? "tree " : "tab " ; + NarratorFactory.getInstance().speak(componentType + next.getAccessibleContext().getAccessibleName()); + InteractionLog.log("TABBED PANE","change focus to "+componentType,next.getAccessibleContext().getAccessibleName()); + } + }); + + /* same thing with the back tab */ + for(AWTKeyStroke keyStroke : KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)) + getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(keyStroke.getKeyCode(),keyStroke.getModifiers()),"back_tab"); + + getActionMap().put("back_tab", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FocusTraversalPolicy policy = KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalPolicy(); + Component previous = policy.getComponentBefore(EditorTabbedPane.this.frame, KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); + previous.requestFocusInWindow(); + String componentType = (previous instanceof JTree) ? "tree " : "tab " ; + NarratorFactory.getInstance().speak(componentType+previous.getAccessibleContext().getAccessibleName()); + InteractionLog.log("TABBED PANE","change focus to "+componentType,previous.getAccessibleContext().getAccessibleName()); + } + }); + + /* shut up the narrator upon pressing ctrl */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"ctrldown"); + getActionMap().put("ctrldown",new AbstractAction(){ + public void actionPerformed(ActionEvent evt){ + NarratorFactory.getInstance().shutUp(); + } + }); + } + + public void setComponentTabTitle(Component component, String title){ + int index = indexOfComponent(component); + if(index == -1) + return; + setTitleAt(index,title); + } + + public String getComponentTabTitle(Component component){ + int index = indexOfComponent(component); + if(index == -1) + return null; + return getTitleAt(index); + } + + public void refreshComponentTabTitle(Component component){ + setComponentTabTitle(component,component.getName()); + } + + @Override + public DiagramPanel getComponentAt(int n){ + return (DiagramPanel)super.getComponent(n); + } + + public int getDiagramNameTabIndex(String diagramName){ + for(int i=0; i<getTabCount();i++){ + DiagramPanel dPanel = getComponentAt(i); + if(diagramName.equals(dPanel.getDiagram().getName())){ + return i; + } + } + return -1; + } + + public int getPathTabIndex(String path){ + for(int i=0; i<getTabCount();i++){ + DiagramPanel dPanel = getComponentAt(i); + if(path.equals(dPanel.getFilePath())){ + return i; + } + } + return -1; + } + + private JFrame frame; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/ExtensionFilter.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,86 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.io.File; +import java.util.StringTokenizer; +import javax.swing.filechooser.FileFilter; + +/** + A file filter that accepts all files with a given set + of extensions. +*/ +public class ExtensionFilter + extends FileFilter +{ + /** + Constructs an extension file filter. + @param description the description (e.g. "Woozle files") + @param extensions the accepted extensions (e.g. + new String[] { ".woozle", ".wzl" }) + */ + public ExtensionFilter(String description, + String[] extensions){ + this.description = description; + this.extensions = extensions; + } + + /** + Constructs an extension file filter. + @param description the description (e.g. "Woozle files") + */ + public ExtensionFilter(String description, + String extensions){ + this.description = description; + StringTokenizer tokenizer = new StringTokenizer( + extensions, "|"); + this.extensions = new String[tokenizer.countTokens()]; + for (int i = 0; i < this.extensions.length; i++) + this.extensions[i] = tokenizer.nextToken(); + } + + @Override + public boolean accept(File f){ + if (f.isDirectory()) return true; + String fname = f.getName().toLowerCase(); + for (int i = 0; i < extensions.length; i++) + if (fname.endsWith(extensions[i].toLowerCase())) + return true; + return false; + } + + @Override + public String getDescription(){ + return description; + } + + public String[] getExtensions(){ + return extensions; + } + + @Override + public String toString(){ + return description; + } + + private String description; + private String[] extensions; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/FileService.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,254 @@ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Frame; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ResourceBundle; + +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; + +import uk.ac.qmul.eecs.ccmi.gui.filechooser.FileChooser; +import uk.ac.qmul.eecs.ccmi.gui.filechooser.FileChooserFactory; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +public abstract class FileService +{ + + /** + * An Open object encapsulates the stream, name and path of the file that the user selected for opening. + */ + public interface Open + { + /** + * Gets the input stream corresponding to the user selection. + * @return the input stream, or null if the user cancels the file selection task + */ + InputStream getInputStream() throws IOException ; + /** + * Gets the name of the file that the user selected. + * @return the file name, or null if the user cancels the file selection task + */ + String getName() throws IOException ; + + /** + * Gets the path of the file that the user selected. + * @return the file path , or null if the user cancels the file selection task + */ + String getPath() throws IOException; + + } + + /** + * A Save object encapsulates the stream and name of the file that the user selected for saving. + */ + public interface Save + { + /** + * Gets the output stream corresponding to the user selection. + * @return the output stream, or null if the user cancels the file selection task + */ + OutputStream getOutputStream(); + /** + * Gets the name of the file that the user selected. + * @return the file name, or null if the user cancels the file selection task + */ + String getName(); + /** + * Gets the path of the file that the user selected. + * @return the file path, or null if the user cancels the file selection task + */ + String getPath(); + } + + /** + * This class implements a FileService with a JFileChooser + */ + public static class ChooserService + { + public ChooserService(File initialDirectory){ + useAccessible = Boolean.parseBoolean(PreferencesService.getInstance().get("use_accessible_filechooser", "true")); + fileChooser = FileChooserFactory.getFileChooser(useAccessible); + fileChooser.setCurrentDirectory(initialDirectory); + } + + /* If the user cancels the task (presses cancel button or the X at the top left) * + * the CANCEl sound is played (together with the registered playerListeenr if any) */ + public FileService.Open open(String defaultDirectory, String defaultFile, + ExtensionFilter filter, Frame frame) throws FileNotFoundException { + fileChooser.resetChoosableFileFilters(); + fileChooser.setFileFilter(filter); + if (defaultDirectory != null) + fileChooser.setCurrentDirectory(new File(defaultDirectory)); + if (defaultFile == null) + fileChooser.setSelectedFile(null); + else + fileChooser.setSelectedFile(new File(defaultFile)); + int response = fileChooser.showOpenDialog(frame); + if (response == JFileChooser.APPROVE_OPTION) + return new OpenImpl(fileChooser.getSelectedFile()); + else{ + if(useAccessible) + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return new OpenImpl(null); + } + } + + + /* If the user cancels the task (presses cancel button or the X at the top left) * + * the CANCEl sound is played (together with the registered playerListeenr if any) */ + public FileService.Save save(String defaultDirectory, String defaultFile, + ExtensionFilter filter, String removeExtension, String addExtension) throws FileNotFoundException { + fileChooser.resetChoosableFileFilters(); + fileChooser.setFileFilter(filter); + if (defaultDirectory == null) + fileChooser.setCurrentDirectory(new File(".")); + else + fileChooser.setCurrentDirectory(new File(defaultDirectory)); + if (defaultFile != null){ + File f = new File(editExtension(defaultFile, removeExtension, addExtension)); + if(f.exists()) + fileChooser.setSelectedFile(f); + else + fileChooser.setSelectedFile(null); + }else + fileChooser.setSelectedFile(null); + int response = fileChooser.showSaveDialog(null); + if (response == JFileChooser.APPROVE_OPTION){ + File f = fileChooser.getSelectedFile(); + if (addExtension != null && f.getName().indexOf(".") < 0) // no extension supplied + f = new File(f.getPath() + addExtension); + if (!f.exists()) return new SaveImpl(f); + + /* file with this name already exists, we must ask the user to confirm */ + ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + if(useAccessible){ + int result = SpeechOptionPane.showConfirmDialog( + null, + resources.getString("dialog.overwrite"), + SpeechOptionPane.YES_NO_OPTION); + if (result == SpeechOptionPane.YES_OPTION) + return new SaveImpl(f); + }else{ + int result = JOptionPane.showConfirmDialog( + null, + resources.getString("dialog.overwrite"), + null, + JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.YES_OPTION) + return new SaveImpl(f); + } + } + if(useAccessible) + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return new SaveImpl(null); + } + + private FileChooser fileChooser; + private boolean useAccessible; + } + + public static class DirectService { + public Open open(File file) throws IOException{ + return new OpenImpl(file); + } + + public Save save(File file) throws IOException{ + return new SaveImpl(file); + } + } + + private static class SaveImpl implements FileService.Save{ + public SaveImpl(File f) throws FileNotFoundException{ + if (f != null){ + path = f.getPath(); + name = getFileNameFromPath(path,false); + out = new BufferedOutputStream(new FileOutputStream(f)); + } + } + + @Override + public String getName() { return name; } + @Override + public String getPath() {return path; } + @Override + public OutputStream getOutputStream() { return out; } + + private String name; + private String path; + private OutputStream out; + } + + private static class OpenImpl implements FileService.Open + { + public OpenImpl(File f) throws FileNotFoundException{ + if (f != null){ + path = f.getPath(); + name = getFileNameFromPath(path,false); + in = new BufferedInputStream(new FileInputStream(f)); + } + } + + @Override + public String getName() { return name; } + @Override + public String getPath() { return path; } + @Override + public InputStream getInputStream() { return in; } + + private String path; + private String name; + private InputStream in; + } + + /** + Edits the file path so that it ends in the desired + extension. + @param original the file to use as a starting point + @param toBeRemoved the extension that is to be + removed before adding the desired extension. Use + null if nothing needs to be removed. + @param desired the desired extension (e.g. ".png"), + or a | separated list of extensions + @return original if it already has the desired + extension, or a new file with the edited file path + */ + public static String editExtension(String original, + String toBeRemoved, String desired){ + if (original == null) return null; + int n = desired.indexOf('|'); + if (n >= 0) desired = desired.substring(0, n); + String path = original; + if (!path.toLowerCase().endsWith(desired.toLowerCase())){ + if (toBeRemoved != null && path.toLowerCase().endsWith( + toBeRemoved.toLowerCase())) + path = path.substring(0, path.length() - toBeRemoved.length()); + path = path + desired; + } + return path; + } + + public static String getFileNameFromPath(String path,boolean keepExtension){ + int index = path.lastIndexOf(System.getProperty("file.separator")); + String name; + if(index == -1) + name = path; + else + name = path.substring(index+1); + if(!keepExtension){ + index = name.lastIndexOf('.'); + if(index != -1) + name = name.substring(0, index); + } + return name; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Finder.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,114 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; +import java.util.Collection; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; + +/** + * + * A utility class which provides methods for searching either a node or an edge + * in a collection or array. + * + */ +public abstract class Finder { + public static Node findNode(String nodeClass,Node[] prototypes){ + for(Node n : prototypes){ + if(n.getType().equals(nodeClass)){ + return n; + } + } + return null; + } + + public static Edge findEdge(String edgeClass,Edge[] prototypes){ + for(Edge e : prototypes){ + if(e.getType().equals(edgeClass)){ + return e; + } + } + return null; + } + + public static Node findNode(Long id, Collection<Node> collection){ + for(Node n : collection) + if(n.getId() == id) + return n; + return null; + } + + public static Node findNode(Point2D p, Collection<Node> collection){ + for (Node n : collection) + if (n.contains(p)) + return n; + return null; + } + + public static Edge findEdge(Point2D p, Collection<Edge> collection){ + for (Edge e : collection) + if (e.contains(p)) + return e; + return null; + } + + public static Edge findEdge(Long id, Collection<Edge> collection){ + for(Edge e : collection) + if(e.getId() == id) + return e; + return null; + } + + public static DiagramElement findElement(Long id, Collection<DiagramElement> collection){ + for(DiagramElement e : collection) + if(e.getId() == id) + return e; + return null; + } + + public static DiagramElement findElementByHashcode(long identityHashcode, Collection<DiagramElement> collection){ + for(DiagramElement de : collection){ + if(System.identityHashCode(de) == identityHashcode){ + return de; + } + } + return null; + } + + /** + * Return the tree node whose path is described by the variable path + * where path contains the indexes returned by each node n of the path upon calling n.getParent().getChildAt(n) + * + * @param path + * @param root + * @return + */ + public static DiagramModelTreeNode findTreeNode(int[] path, DiagramModelTreeNode root){ + DiagramModelTreeNode retVal = root; + for(int i=0;i<path.length;i++){ + if(retVal.getChildCount() <= path[i]) + return null; + retVal = retVal.getChildAt(path[i]); + } + return retVal; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphElement.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,47 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Graphics2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + + +/** + * An interface implemented by {@link Node} and {@link Edge} and it defines methods that + * both the classes implements as they're object painted on a graph. The interface is used mainly + * for convenience in treating the two types of object in a unified way. + * + */ +public interface GraphElement { + public void draw(Graphics2D g2); + + public void translate(Point2D p, double dx, double dy); + + public void stopMove(); + + public void startMove(Point2D p); + + public Rectangle2D getBounds(); + + public Point2D getConnectionPoint(Direction d); + + public boolean contains(Point2D p); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphPanel.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,757 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.AbstractAction; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.KeyStroke; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionListener; +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * A panel to draw a graph + */ +@SuppressWarnings("serial") +public class GraphPanel extends JPanel{ + /** + * Constructs a graph. + * @param aDiagram a diagram to paint in the graph + * @param a aToolbar a toolbar containing the node and edges prototypes for creating + * elements in the graph. + */ + + public GraphPanel(Diagram aDiagram, GraphToolbar aToolbar) { + grid = new Grid(); + gridSize = GRID; + grid.setGrid((int) gridSize, (int) gridSize); + zoom = 1; + toolbar = aToolbar; + setBackground(Color.WHITE); + wasMoving = false; + minBounds = null; + + this.model = aDiagram.getCollectionModel(); + synchronized(model.getMonitor()){ + edges = new LinkedList<Edge>(model.getEdges()); + nodes = new LinkedList<Node>(model.getNodes()); + } + setModelUpdater(aDiagram.getModelUpdater()); + + selectedElements = new HashSet<DiagramElement>(); + moveLockedElements = new HashSet<Object>(); + + toolbar.addEdgeCreatedListener(new innerEdgeListener()); + + getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0),"delete"); + getActionMap().put("delete", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + /* nothing selected DELETE key has no effect */ + if(selectedElements.isEmpty()) + return; + /* create a new Set to maintain iterator consistency as elementTakenOut will change selectedItems */ + HashSet<DiagramElement> iterationSet = new HashSet<DiagramElement>(selectedElements); + HashSet<DiagramElement>alreadyLockedElements = new HashSet<DiagramElement>(); + /* check which, of the selected elements, can be deleted and which ones are currently held by * + * other clients. If an element is locked it's removed from the list and put into a separated set */ + for(Iterator<DiagramElement> itr=iterationSet.iterator(); itr.hasNext();){ + DiagramElement selected = itr.next(); + if(!modelUpdater.getLock(selected, Lock.DELETE)){ + itr.remove(); + alreadyLockedElements.add(selected); + } + } + ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + /* all the elements are locked by other clients */ + if(iterationSet.isEmpty()){ + iLog("Could not get lock on any selected element for deletion",""); + JOptionPane.showMessageDialog( + JOptionPane.getFrameForComponent(GraphPanel.this), + alreadyLockedElements.size() == 1 ? // singular vs plural + resources.getString("dialog.lock_failure.delete") : + resources.getString("dialog.lock_failure.deletes")); + return; + } + + String warning = ""; + if(!alreadyLockedElements.isEmpty()){ + StringBuilder builder = new StringBuilder(resources.getString("dialog.lock_failure.deletes_warning")); + for(DiagramElement alreadyLocked : alreadyLockedElements) + builder.append(alreadyLocked.getName()).append(' '); + warning = builder.append('\n').toString(); + iLog("Could not get lock on some selected element for deletion",warning); + } + + iLog("open delete dialog",warning); + int answer = JOptionPane.showConfirmDialog( + JOptionPane.getFrameForComponent(GraphPanel.this), + warning+resources.getString("dialog.confirm.deletions"), + resources.getString("dialog.confirm.title"), + SpeechOptionPane.YES_NO_OPTION); + if(answer == JOptionPane.YES_OPTION){ + /* the user chose to delete the elements, proceed (locks * + * will be automatically removed upon deletion by the server ) */ + for(DiagramElement selected : iterationSet) + modelUpdater.takeOutFromCollection(selected); + }else{ + /* the user chose not to delete the elements, release the acquired locks */ + for(DiagramElement selected : iterationSet){ + /* if it's a node all its attached edges were locked as well */ + /*if(selected instanceof Node){ DONE IN THE SERVER + Node n = (Node)selected; + for(int i=0; i<n.getEdgesNum();i++){ + modelUpdater.yieldLock(n.getEdgeAt(i), Lock.DELETE); + } + }*/ + modelUpdater.yieldLock(selected, Lock.DELETE); + } + iLog("cancel delete node dialog",""); + } + }}); + + /* ---- COLLECTION LISTENER ---- + * Adding a collection listener. This listener reacts at changes in the model + * by any source, and thus the graph itself. Basically it refreshes the graph + * and paints again all the nodes and edges. + */ + model.addCollectionListener(new CollectionListener(){ + @Override + public void elementInserted(final CollectionEvent e) { + DiagramElement element = e.getDiagramElement(); + if(element instanceof Node) + nodes.add((Node)element); + else + edges.add((Edge)element); + checkBounds(element,false); + if(e.getDiagramElement() instanceof Node && e.getSource().equals(model) ){ //FIXME change model into this model source changes + setElementSelected(e.getDiagramElement()); + dragMode = DRAG_NODE; + } + revalidate(); + repaint(); + } + @Override + public void elementTakenOut(final CollectionEvent e) { + DiagramElement element = e.getDiagramElement(); + if(element instanceof Node){ + if(nodePopup != null && nodePopup.nodeRef.equals(element)) + nodePopup.setVisible(false); + nodes.remove(element); + } + else{ + if(edgePopup != null && edgePopup.edgeRef.equals(element)) + edgePopup.setVisible(false); + edges.remove(element); + } + checkBounds(e.getDiagramElement(),true); + removeElementFromSelection(e.getDiagramElement()); + revalidate(); + repaint(); + } + @Override + public void elementChanged(final ElementChangedEvent e) { + /* we changed the position of an element and might need to update the boundaries */ + if(e.getChangeType().equals("stop_move")){ + checkBounds(e.getDiagramElement(),false); + } + revalidate(); + repaint(); + } + }); + /* --------------------------------------------------------------------------- */ + + /* ------------- MOUSE LISTENERS -------------------------------------------- + * For pressed and released mouse click and moved mouse + */ + addMouseListener(new MouseAdapter(){ + @Override + public void mousePressed(MouseEvent event){ + requestFocusInWindow(); + final Point2D mousePoint = new Point2D.Double( + (event.getX()+minX)/zoom, + (event.getY()+minY)/zoom + ); + boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; + Node n = Finder.findNode(mousePoint,nodes); + Edge e = Finder.findEdge(mousePoint,edges); + + Object tool = toolbar.getSelectedTool(); + /* - right click - */ + if((event.getModifiers() & InputEvent.BUTTON1_MASK) == 0) { + if(e != null){ + if( e.contains(mousePoint)){ + Node extremityNode = e.getClosestNode(mousePoint,EDGE_END_MIN_CLICK_DIST); + if(extremityNode == null){ // click far from the attached nodes, only prompt with set name item + EdgePopupMenu pop = new EdgePopupMenu(e,GraphPanel.this,modelUpdater); + edgePopup = pop; + pop.show(GraphPanel.this, event.getX(), event.getY()); + }else{ // click near an attached nodes, prompt for name change, set end label and select arrow head + EdgePopupMenu pop = new EdgePopupMenu(e,extremityNode,GraphPanel.this,modelUpdater); + edgePopup = pop; + pop.show(GraphPanel.this, event.getX(), event.getY()); + } + } + }else if(n != null){ + NodePopupMenu pop = new NodePopupMenu(n,GraphPanel.this,modelUpdater); + nodePopup = pop; + pop.show(GraphPanel.this, event.getX(), event.getY()); + }else + return; + } + + /* - one click && palette == select - */ + else if (tool == null){ + if(n != null){ // node selected + if (isCtrl) + addElementToSelection(n,false); + else + setElementSelected(n); + dragMode = DRAG_NODE; + }else if (e != null){ // edge selected + if (isCtrl){ + addElementToSelection(e,false); + dragMode = DRAG_NODE; + }else{ + setElementSelected(e); + modelUpdater.startMove(e, mousePoint); + dragMode = DRAG_EDGE; + } + }else{ // nothing selected : make selection lasso + if (!isCtrl) + clearSelection(); + dragMode = DRAG_LASSO; + } + } + /* - one click && palette == node - */ + else { + /* click on an already existing node = select it*/ + if (n != null){ + if (isCtrl) + addElementToSelection(n,false); + else + setElementSelected(n); + dragMode = DRAG_NODE; + }else{ + Node prototype = (Node) tool; + Node newNode = (Node) prototype.clone(); + Rectangle2D bounds = newNode.getBounds(); + /* perform the translation from the origin */ + newNode.translate(new Point2D.Double(), mousePoint.getX() - bounds.getX(), + mousePoint.getY() - bounds.getY()); + /* log stuff */ + iLog("insert node",""+((newNode.getId() == DiagramElement.NO_ID) ? "(no id)" : newNode.getId())); + /* insert the node into the model (no lock needed) */ + modelUpdater.insertInCollection(newNode); + } + } + + lastMousePoint = mousePoint; + mouseDownPoint = mousePoint; + repaint(); + } + + @Override + public void mouseReleased(MouseEvent event){ + final Point2D mousePoint = new Point2D.Double( + (event.getX()+minX)/zoom, + (event.getY()+minY)/zoom + ); + if(lastSelected != null){ + if(lastSelected instanceof Node){ + if(wasMoving){ + iLog("move selected stop",mousePoint.getX()+" "+ mousePoint.getY()); + for(Object element : moveLockedElements){ + modelUpdater.stopMove((GraphElement)element); + modelUpdater.yieldLock((DiagramModelTreeNode)element, Lock.MOVE); + } + moveLockedElements.clear(); + } + }else{ // instanceof Edge + if(wasMoving){ + iLog("bend edge stop",mousePoint.getX()+" "+ mousePoint.getY()); + if(moveLockedEdge != null){ + modelUpdater.stopMove(moveLockedEdge); + modelUpdater.yieldLock(moveLockedEdge, Lock.MOVE); + moveLockedEdge = null; + } + } + } + } + dragMode = DRAG_NONE; + wasMoving = false; + repaint(); + } + }); + + addMouseMotionListener(new MouseMotionAdapter(){ + public void mouseDragged(MouseEvent event){ + Point2D mousePoint = new Point2D.Double( + (event.getX()+minX)/zoom, + (event.getY()+minY)/zoom + ); + boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; + + if (dragMode == DRAG_NODE){ + /* translate selected nodes (edges as well) */ + double dx = mousePoint.getX() - lastMousePoint.getX(); + double dy = mousePoint.getY() - lastMousePoint.getY(); + if(!wasMoving){ + wasMoving = true; + /* when the motion starts, we need to get the move-lock from the server */ + Iterator<DiagramElement> iterator = selectedElements.iterator(); + while(iterator.hasNext()){ + DiagramElement element = iterator.next(); + if(modelUpdater.getLock(element, Lock.MOVE)){ + moveLockedElements.add(element); + }else{ + iLog("Could not get move lock for element",DiagramElement.toLogString(element)); + iterator.remove(); + } + } + iLog("move selected start",mousePoint.getX()+" "+ mousePoint.getY()); + } + + for (DiagramElement selected : selectedElements){ + if(selected instanceof Node) + modelUpdater.translate((Node)selected, lastMousePoint, dx, dy); + else + modelUpdater.translate((Edge)selected, lastMousePoint, dx, dy); + } + } else if(dragMode == DRAG_EDGE){ + if(!wasMoving){ + wasMoving = true; + if(modelUpdater.getLock(lastSelected, Lock.MOVE)) + moveLockedEdge = (Edge)lastSelected; + else + iLog("Could not get move lock for element",DiagramElement.toLogString(lastSelected)); + iLog("bend edge start",mousePoint.getX()+" "+ mousePoint.getY()); + } + if(moveLockedEdge != null) + modelUpdater.bend(moveLockedEdge, new Point2D.Double(mousePoint.getX(), mousePoint.getY())); + } else if (dragMode == DRAG_LASSO){ + double x1 = mouseDownPoint.getX(); + double y1 = mouseDownPoint.getY(); + double x2 = mousePoint.getX(); + double y2 = mousePoint.getY(); + Rectangle2D.Double lasso = new Rectangle2D.Double(Math.min(x1, x2), + Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2)); + for (Node n : GraphPanel.this.nodes){ + Rectangle2D bounds = n.getBounds(); + if(!isCtrl && !lasso.contains(bounds)){ + removeElementFromSelection(n); + } + else if (lasso.contains(bounds)){ + addElementToSelection(n,true); + } + } + if(selectedElements.size() != oldLazoSelectedNum){ + StringBuilder builder = new StringBuilder(); + for(DiagramElement de : selectedElements) + builder.append(DiagramElement.toLogString(de)).append(' '); + iLog("added by lazo",builder.toString()); + } + oldLazoSelectedNum = selectedElements.size(); + } + lastMousePoint = mousePoint; + } + }); + } + /* --------------------------------------------------------------------------- */ + + @Override + public void paintComponent(Graphics g){ + super.paintComponent(g); + paintGraph(g); + } + + public void paintGraph(Graphics g){ + Graphics2D g2 = (Graphics2D) g; + g2.translate(-minX, -minY); + g2.scale(zoom, zoom); + Rectangle2D bounds = getBounds(); + Rectangle2D graphBounds = getGraphBounds(); + if (!hideGrid) grid.draw(g2, new Rectangle2D.Double(minX, minY, + Math.max(bounds.getMaxX() / zoom, graphBounds.getMaxX()), + Math.max(bounds.getMaxY() / zoom, graphBounds.getMaxY()))); + + /* draw nodes and edges */ + for (Edge e : edges) + e.draw(g2); + for (Node n : nodes) + n.draw(g2); + + for(DiagramElement selected : selectedElements){ + if (selected instanceof Node){ + Rectangle2D grabberBounds = ((Node) selected).getBounds(); + drawGrabber(g2, grabberBounds.getMinX(), grabberBounds.getMinY()); + drawGrabber(g2, grabberBounds.getMinX(), grabberBounds.getMaxY()); + drawGrabber(g2, grabberBounds.getMaxX(), grabberBounds.getMinY()); + drawGrabber(g2, grabberBounds.getMaxX(), grabberBounds.getMaxY()); + } + else if (selected instanceof Edge){ + for(Point2D p : ((Edge)selected).getConnectionPoints()) + drawGrabber(g2, p.getX(), p.getY()); + } + } + + if (dragMode == DRAG_LASSO){ + Color oldColor = g2.getColor(); + g2.setColor(GRABBER_COLOR); + double x1 = mouseDownPoint.getX(); + double y1 = mouseDownPoint.getY(); + double x2 = lastMousePoint.getX(); + double y2 = lastMousePoint.getY(); + Rectangle2D.Double lasso = new Rectangle2D.Double(Math.min(x1, x2), + Math.min(y1, y2), Math.abs(x1 - x2) , Math.abs(y1 - y2)); + g2.draw(lasso); + g2.setColor(oldColor); + repaint(); + } + } + + /** + * Draws a single "grabber", a filled square + * @param g2 the graphics context + * @param x the x coordinate of the center of the grabber + * @param y the y coordinate of the center of the grabber + */ + static void drawGrabber(Graphics2D g2, double x, double y){ + final int SIZE = 5; + Color oldColor = g2.getColor(); + g2.setColor(GRABBER_COLOR); + g2.fill(new Rectangle2D.Double(x - SIZE / 2, y - SIZE / 2, SIZE, SIZE)); + g2.setColor(oldColor); + } + + @Override + public Dimension getPreferredSize(){ + Rectangle2D graphBounds = getGraphBounds(); + return new Dimension((int) (zoom * graphBounds.getMaxX()), + (int) (zoom * graphBounds.getMaxY())); + } + + /** + * Changes the zoom of this panel. The zoom is 1 by default and is multiplied + * by sqrt(2) for each positive stem or divided by sqrt(2) for each negative + * step. + * @param steps the number of steps by which to change the zoom. A positive + * value zooms in, a negative value zooms out. + */ + public void changeZoom(int steps){ + final double FACTOR = Math.sqrt(2); + for (int i = 1; i <= steps; i++) + zoom *= FACTOR; + for (int i = 1; i <= -steps; i++) + zoom /= FACTOR; + revalidate(); + repaint(); + } + + /** + * Changes the grid size of this panel. The zoom is 10 by default and is + * multiplied by sqrt(2) for each positive stem or divided by sqrt(2) for + * each negative step. + * @param steps the number of steps by which to change the zoom. A positive + * value zooms in, a negative value zooms out. + */ + public void changeGridSize(int steps){ + final double FACTOR = Math.sqrt(2); + for (int i = 1; i <= steps; i++) + gridSize *= FACTOR; + for (int i = 1; i <= -steps; i++) + gridSize /= FACTOR; + grid.setGrid((int) gridSize, (int) gridSize); + repaint(); + } + + private void addElementToSelection(DiagramElement element, boolean byLasso){ + /* if not added to selected elements by including it in the lasso, the element is moved * + * to the back of the collection so that it will be painted on the top on the next refresh */ + if(!byLasso) + if(element instanceof Node){ + /* put the node in the last position so that it will be drawn on the top */ + nodes.remove(element); + nodes.add((Node)element); + iLog("addeded node to selected",DiagramElement.toLogString(element)); + }else{ + /* put the edge in the last position so that it will be drawn on the top */ + edges.remove(element); + edges.add((Edge)element); + iLog("addeded edge to selected",DiagramElement.toLogString(element)); + } + if(selectedElements.contains(element)){ + lastSelected = element; + return; + } + lastSelected = element; + selectedElements.add(element); + return; + } + + private void removeElementFromSelection(DiagramElement element){ + if (element == lastSelected){ + lastSelected = null; + } + if(selectedElements.contains(element)){ + selectedElements.remove(element); + } + } + + private void setElementSelected(DiagramElement element){ + /* clear the selection */ + selectedElements.clear(); + lastSelected = element; + selectedElements.add(element); + if(element instanceof Node){ + nodes.remove(element); + nodes.add((Node)element); + iLog("node selected",DiagramElement.toLogString(element)); + }else{ + edges.remove(element); + edges.add((Edge)element); + iLog("edge selected",DiagramElement.toLogString(element)); + } + } + + private void clearSelection(){ + iLog("selection cleared",""); + selectedElements.clear(); + lastSelected = null; + } + + /** + * Sets the value of the hideGrid property + * @param newValue true if the grid is being hidden + */ + public void setHideGrid(boolean newValue){ + hideGrid = newValue; + repaint(); + } + + /** + * Gets the value of the hideGrid property + * @return true if the grid is being hidden + */ + public boolean getHideGrid(){ + return hideGrid; + } + + /** + Gets the smallest rectangle enclosing the graph + @return the bounding rectangle + */ + public Rectangle2D getMinBounds() { return minBounds; } + public void setMinBounds(Rectangle2D newValue) { minBounds = newValue; } + + public Rectangle2D getGraphBounds(){ + Rectangle2D r = minBounds; + for (Node n : nodes){ + Rectangle2D b = n.getBounds(); + if (r == null) r = b; + else r.add(b); + } + for (Edge e : edges){ + r.add(e.getBounds()); + } + return r == null ? new Rectangle2D.Double() : new Rectangle2D.Double(r.getX(), r.getY(), + r.getWidth() + Node.SHADOW_GAP + Math.abs(minX), r.getHeight() + Node.SHADOW_GAP + Math.abs(minY)); + } + + public void setModelUpdater(DiagramModelUpdater modelUpdater){ + this.modelUpdater = modelUpdater; + } + + private void iLog(String action,String args){ + InteractionLog.log("GRAPH",action,args); + } + + private void checkBounds(DiagramElement de, boolean wasRemoved){ + GraphElement ge; + if(de instanceof Node) + ge = (Node)de; + else + ge = (Edge)de; + if(wasRemoved){ + if(ge == top){ + top = null; + minY = 0; + Rectangle2D bounds; + for(Edge e : edges){ + bounds = e.getBounds(); + if(bounds.getY() < minY){ + top = e; + minY = bounds.getY(); + } + } + for(Node n : nodes){ + bounds = n.getBounds(); + if(bounds.getY() < minY){ + top = n; + minY = bounds.getY(); + } + } + } + if(ge == left){ + minX = 0; + left = null; + synchronized(model.getMonitor()){ + Rectangle2D bounds; + for(Edge e : model.getEdges()){ + bounds = e.getBounds(); + if(bounds.getX() < minX){ + left = e; + minX = bounds.getX(); + } + } + for(Node n : model.getNodes()){ + bounds = n.getBounds(); + if(bounds.getX() < minX){ + left = n; + minX = bounds.getX(); + } + } + } + } + }else{ // was added or translated + Rectangle2D bounds = ge.getBounds(); + if(top == null){ + if(bounds.getY() < 0){ + top = ge; + minY = bounds.getY(); + } + }else if(ge == top){ //the top-most has been translated recalculate the new top-most, as itf it were deleted + checkBounds(de, true); + }else if(bounds.getY() < top.getBounds().getY()){ + top = ge; + minY = bounds.getY(); + } + + if(left == null){ + if(bounds.getX() < 0){ + left = ge; + minX = bounds.getX(); + } + }else if(ge == left){ + checkBounds(de,true);//the left-most has been translated recalculate the new left-most, as if it were deleted + } + else if(bounds.getX() < left.getBounds().getX()){ + left = ge; + minX = bounds.getX(); + } + } + } + + private class innerEdgeListener implements GraphToolbar.EdgeCreatedListener { + @Override + public void edgeCreated(Edge e) { + ArrayList<DiagramNode> nodesToConnect = new ArrayList<DiagramNode>(selectedElements.size()); + for(DiagramElement element : selectedElements){ + if(element instanceof Node) + nodesToConnect.add((Node)element); + } + try { + e.connect(nodesToConnect); + modelUpdater.insertInCollection(e); + } catch (ConnectNodesException cnEx) { + JOptionPane.showMessageDialog(GraphPanel.this, + cnEx.getLocalizedMessage(), + ResourceBundle.getBundle(EditorFrame.class.getName()).getString("dialog.error.title"), + JOptionPane.ERROR_MESSAGE); + iLog("insert edge error",cnEx.getMessage()); + } + } + } + + private List<Edge> edges; + private List<Node> nodes; + private DiagramModelUpdater modelUpdater; + private CollectionModel<Node,Edge> model; + + private Grid grid; + private GraphToolbar toolbar; + private NodePopupMenu nodePopup; + private EdgePopupMenu edgePopup; + + private double zoom; + private double gridSize; + private boolean hideGrid; + private boolean wasMoving; + + private GraphElement top; + private GraphElement left; + private double minX; + private double minY; + + private DiagramElement lastSelected; + private Edge moveLockedEdge; + private Set<DiagramElement> selectedElements; + private Set<Object> moveLockedElements; + + private Point2D lastMousePoint; + private Point2D mouseDownPoint; + private Rectangle2D minBounds; + private int dragMode; + + private int oldLazoSelectedNum; + + /* button is not down, mouse motion will habe no effects */ + private static final int DRAG_NONE = 0; + /* one or more nodes (and eventually some edges) have been selected, mouse motion will result in a translation */ + private static final int DRAG_NODE = 1; + /* one edge has been selected, mouse motion will result in an edge bending */ + private static final int DRAG_EDGE = 2; + /* mouse button down but nothing selected, mouse motion will result in a lasso */ + private static final int DRAG_LASSO = 3; // multiple selection + + private static final int GRID = 10; + private static final double EDGE_END_MIN_CLICK_DIST = 10; + + public static final Color GRABBER_COLOR = new Color(0,128,255); +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphToolbar.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,231 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.geom.AffineTransform; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.Enumeration; +import java.util.ResourceBundle; + +import javax.swing.ButtonGroup; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; + + +/** + A Toolbar that contains node and edge prototype icons. By using the toolbar + the user can create nodes and edges in the diagram out of clonation from the prototype. +*/ +@SuppressWarnings("serial") +public class GraphToolbar extends JToolBar { + /** + Constructs a tool bar with no icons. + */ + public GraphToolbar(Diagram diagram){ + /* creates icon for select button */ + Icon icon = new Icon(){ + public int getIconHeight() { return BUTTON_SIZE; } + public int getIconWidth() { return BUTTON_SIZE; } + public void paintIcon(Component c, Graphics g, + int x, int y){ + Graphics2D g2 = (Graphics2D)g; + GraphPanel.drawGrabber(g2, x + OFFSET, y + OFFSET); + GraphPanel.drawGrabber(g2, x + OFFSET, y + BUTTON_SIZE - OFFSET); + GraphPanel.drawGrabber(g2, x + BUTTON_SIZE - OFFSET, y + OFFSET); + GraphPanel.drawGrabber(g2, x + BUTTON_SIZE - OFFSET, y + BUTTON_SIZE - OFFSET); + } + }; + /* add selection button */ + ResourceBundle resources = + ResourceBundle.getBundle(EditorFrame.class.getName()); + String text = resources.getString("grabber.text"); + selectButton = new NodeButton(null,icon); + selectButton.setToolTipText(text); + nodeButtonsGroup = new ButtonGroup(); + nodeButtonsGroup.add(selectButton); + add(selectButton); + + /* add diagram buttons to the toolbar */ + Node[] nodeTypes = diagram.getNodePrototypes(); + for (int i = 0; i < nodeTypes.length; i++){ + text = nodeTypes[i].getType(); + add(nodeTypes[i], text ); + } + + /* select the select-button as default */ + nodeButtonsGroup.setSelected(selectButton.getModel(), true); + + /* separate node buttons from edge buttons */ + addSeparator(); + + /* add diagram edges to the toolbar */ + Edge[] edgeTypes = diagram.getEdgePrototypes(); + for (int i = 0; i < edgeTypes.length; i++){ + text = edgeTypes[i].getType(); + add(edgeTypes[i], text ); + } + } + + /** + Gets the node prototype that is associated with + the currently selected button + @return a Node or Edge prototype + */ + public Node getSelectedTool() { + @SuppressWarnings("rawtypes") + Enumeration elements = nodeButtonsGroup.getElements(); + while (elements.hasMoreElements()) { + NodeButton b = (NodeButton)elements.nextElement(); + if (b.isSelected()) { + /* switch back to the select-button */ + nodeButtonsGroup.setSelected(selectButton.getModel(), true); + return b.getNode(); + } + } + /* getting here means the selection button is selected */ + return null; + } + + /** + Adds a node to the tool bar. + @param n the node to add + @param tip the tool tip + */ + public void add(final Node n, String text){ + Icon icon = new Icon(){ + public int getIconHeight() { return BUTTON_SIZE; } + public int getIconWidth() { return BUTTON_SIZE; } + public void paintIcon(Component c, Graphics g, + int x, int y){ + double width = n.getBounds().getWidth(); + double height = n.getBounds().getHeight(); + Graphics2D g2 = (Graphics2D)g; + double scaleX = (BUTTON_SIZE - OFFSET)/ width; + double scaleY = (BUTTON_SIZE - OFFSET)/ height; + double scale = Math.min(scaleX, scaleY); + + AffineTransform oldTransform = g2.getTransform(); + g2.translate(x, y); + g2.translate(OFFSET/2*scaleX,OFFSET/2*scaleY); + g2.scale(scale, scale); + g2.setColor(Color.black); + n.draw(g2); + g2.setTransform(oldTransform); + } + }; + + NodeButton button = new NodeButton(n, icon); + button.setToolTipText(text); + + add(button); + nodeButtonsGroup.add(button); + } + + /** + Adds an edge to the tool bar. + @param n the node to add + @param tip the tool tip + */ + public void add(final Edge e, String text){ + Icon icon = new Icon(){ + public int getIconHeight() { return BUTTON_SIZE; } + public int getIconWidth() { return BUTTON_SIZE; } + public void paintIcon(Component c, Graphics g, + int x, int y){ + Graphics2D g2 = (Graphics2D)g; + /* create two points */ + Point2D p = new Point2D.Double(); + Point2D q = new Point2D.Double(); + p.setLocation(OFFSET, OFFSET); + q.setLocation(BUTTON_SIZE - OFFSET, BUTTON_SIZE - OFFSET); + + Line2D line = new Line2D.Double(p,q); + Rectangle2D bounds = new Rectangle2D.Double(); + bounds.add(line.getBounds2D()); + + double width = bounds.getWidth(); + double height = bounds.getHeight(); + double scaleX = (BUTTON_SIZE - OFFSET)/ width; + double scaleY = (BUTTON_SIZE - OFFSET)/ height; + double scale = Math.min(scaleX, scaleY); + + AffineTransform oldTransform = g2.getTransform(); + g2.translate(x, y); + g2.scale(scale, scale); + g2.translate(Math.max((height - width) / 2, 0), Math.max((width - height) / 2, 0)); + + g2.setColor(Color.black); + g2.setStroke(e.getStyle().getStroke()); + g2.draw(line); + g2.setTransform(oldTransform); + } + }; + final JButton button = new JButton(icon); + button.setToolTipText(text); + button.setFocusable(false); + + button.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + edgeCreatedListener.edgeCreated((Edge)e.clone()); + }}); + add(button); + } + + public void addEdgeCreatedListener(EdgeCreatedListener edgeCreatedListener){ + this.edgeCreatedListener = edgeCreatedListener; + } + + private class NodeButton extends JToggleButton{ + public NodeButton(Node node, Icon icon){ + super(icon); + setFocusable(false); + this.node = node; + } + + public Node getNode(){ + return node; + } + Node node; + } + + public interface EdgeCreatedListener { + void edgeCreated(Edge e); + } + + private ButtonGroup nodeButtonsGroup; + private EdgeCreatedListener edgeCreatedListener; + private NodeButton selectButton; + + private static final int BUTTON_SIZE = 30; + private static final int OFFSET = 5; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Grid.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,134 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RectangularShape; + +/** + A grid to which nodes can be "snapped". The + snapping operation moves a point to the nearest grid point. +*/ +public class Grid +{ + /** + Constructs a grid with no grid points. + */ + public Grid() + { + setGrid(0, 0); + } + + /** + Sets the grid point distances in x- and y-direction + @param x the grid point distance in x-direction + @param y the grid point distance in y-direction + */ + public void setGrid(double x, double y) + { + gridx = x; + gridy = y; + } + + /** + Draws this grid inside a rectangle. + @param g2 the graphics context + @param bounds the bounding rectangle + */ + public void draw(Graphics2D g2, Rectangle2D bounds) + { + Color PALE_BLUE = new Color(0.9F, 0.8F, 0.9F); + Color oldColor = g2.getColor(); + g2.setColor(PALE_BLUE); + Stroke oldStroke = g2.getStroke(); + for (double x = bounds.getX(); x < bounds.getMaxX(); x += gridx) + g2.draw(new Line2D.Double(x, bounds.getY(), x, bounds.getMaxY())); + for (double y = bounds.getY(); y < bounds.getMaxY(); y += gridy) + g2.draw(new Line2D.Double(bounds.getX(), y, bounds.getMaxX(), y)); + g2.setStroke(oldStroke); + g2.setColor(oldColor); + } + + /** + Snaps a point to the nearest grid point + @param p the point to snap. After the call, the + coordinates of p are changed so that p falls on the grid. + */ + public void snap(Point2D p) + { + double x; + if (gridx == 0) + x = p.getX(); + else + x = Math.round(p.getX() / gridx) * gridx; + double y; + if (gridy == 0) + y = p.getY(); + else + y = Math.round(p.getY() / gridy) * gridy; + + p.setLocation(x, y); + } + + /** + Snaps a rectangle to the nearest grid points + @param r the rectangle to snap. After the call, the + coordinates of r are changed so that all of its corners + falls on the grid. + */ + public void snap(RectangularShape r) + { + double x; + double w; + w = r.getWidth(); + if (gridx == 0) + { + x = r.getX(); + } + else + { + x = Math.round(r.getX() / gridx) * gridx; +// w = Math.ceil(r.getWidth() / (2 * gridx)) * (2 * gridx); + } + double y; + double h; + h = r.getHeight(); + if (gridy == 0) + { + y = r.getY(); + } + else + { + y = Math.round(r.getY() / gridy) * gridy; +// h = Math.ceil(r.getHeight() / (2 * gridy)) * (2 * gridy); + } + + r.setFrame(x, y, w, h); + } + + private double gridx; + private double gridy; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/HapticKindle.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,217 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import javax.swing.SwingUtilities; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.haptics.HapticListener; +import uk.ac.qmul.eecs.ccmi.haptics.HapticListenerCommand; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * + * An instance of HapticListener for the diagram editor. By this class visual diagrams + * can be manipulated by an haptic device. + * + */ +public class HapticKindle extends HapticListener { + + public HapticKindle(){ + super(); + unselectRunnable = new Runnable(){ + @Override + public void run(){ + frame.selectHapticHighligh(null); + } + }; + frameBackupRunnable = new Runnable(){ + @Override + public void run(){ + frame.backupOpenDiagrams(); + } + }; + } + + public void setEditorFrame(EditorFrame frame){ + this.frame = frame; + } + + @Override + public void executeCommand(HapticListenerCommand cmd, int ID, final double x, final double y, final double startX, final double startY) { + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + Object monitor = collectionModel.getMonitor(); + DiagramElement de = null; + switch(cmd){ + case PLAY_ELEMENT_SOUND : + synchronized(monitor){ + de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + } + /* can be null if the tab has been switched or closed in the meantime */ + if(de == null) + return; + SoundFactory.getInstance().play(de.getSound()); + break; + case PLAY_ELEMENT_SPEECH : + synchronized(monitor){ + de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + } + if(de == null) + return; + SoundFactory.getInstance().play(de.getSound()); + NarratorFactory.getInstance().speak(de.getName()); + iLog("touch",((de instanceof Node) ? "node " : "edge ")+de.getName()); + break; + case SELECT : + synchronized(monitor){ + de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + } + if(de == null) + return; + final DiagramElement selectedElement = de; + SwingUtilities.invokeLater(new Runnable(){ + @Override + public void run(){ + frame.selectHapticHighligh(selectedElement); + } + }); + break; + case UNSELECT : + SwingUtilities.invokeLater(unselectRunnable); + break; + case MOVE : + DiagramPanel dPanel = frame.getActiveTab(); + if(dPanel == null) + return; + synchronized(monitor){ + de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + } + if(de == null) + return; + final DiagramElement moveSelectedElement = de; + final DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + if(!modelUpdater.getLock(moveSelectedElement, Lock.MOVE)){ + iLog("Could not get lock on element for motion", DiagramElement.toLogString(moveSelectedElement)); + NarratorFactory.getInstance().speak("Object is being moved by another user"); + return; + } + + try { + SwingUtilities.invokeAndWait(new Runnable(){ + @Override + public void run(){ + if(moveSelectedElement instanceof Node){ + Node n = (Node)moveSelectedElement; + Rectangle2D bounds = n.getBounds(); + Point2D.Double p = new Point2D.Double(bounds.getCenterX(),bounds.getCenterY()); + double dx = x - p.getX(); + double dy = y - p.getY(); + modelUpdater.translate(n, p, dx, dy); + modelUpdater.stopMove(n); + + StringBuilder builder = new StringBuilder(); + builder.append(DiagramElement.toLogString(n)).append(" ").append(p.getX()) + .append(' ').append(p.getY()); + iLog("move node start",builder.toString()); + builder = new StringBuilder(); + builder.append(DiagramElement.toLogString(n)).append(' ') + .append(x).append(' ').append(y); + + iLog("move node end",builder.toString()); + + }else{ + Edge e = (Edge)moveSelectedElement; + modelUpdater.startMove(e, new Point2D.Double(startX,startY)); + Point2D p = new Point2D.Double(x,y); + modelUpdater.bend(e, p); + modelUpdater.stopMove(e); + + StringBuilder builder = new StringBuilder(); + builder.append(DiagramElement.toLogString(e)).append(' ').append(startX) + .append(' ').append(startY); + iLog("bend edge start",builder.toString()); + builder = new StringBuilder(); + builder.append(DiagramElement.toLogString(e)).append(' ') + .append(x).append(' ').append(y); + iLog("bend edge end",builder.toString()); + + } + } + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + SoundFactory.getInstance().play(SoundEvent.HOOK_OFF); + modelUpdater.yieldLock(moveSelectedElement, Lock.MOVE); + break; + case INFO : + synchronized(monitor){ + de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + } + if(de == null) + return; + SoundFactory.getInstance().stop(); + NarratorFactory.getInstance().speak(de.detailedSpokenText()); + iLog("request detailed info",((de instanceof Node) ? "node " : "edge ")+de.getName()); + break; + case PLAY_SOUND : + switch(HapticListenerCommand.Sound.fromInt(ID) ){ + case MAGNET_OFF : + SoundFactory.getInstance().play(SoundEvent.MAGNET_OFF); + iLog("sticky mode off",""); + break; + case MAGNET_ON : + SoundFactory.getInstance().play(SoundEvent.MAGNET_ON); + iLog("sticky mode on",""); + break; + case HOOK_ON : + SoundFactory.getInstance().play(SoundEvent.HOOK_ON); + iLog("hook on",""); + break; + case DRAG : SoundFactory.getInstance().play(SoundEvent.DRAG); + break; + } + break; + case ERROR : + /* no synchronization necessary as the XMLManager looks after it*/ + SwingUtilities.invokeLater(frameBackupRunnable); + NarratorFactory.getInstance().speak("Haptic device crashed. " + + "Unsaved diagrams saved in backup directory"); + break; + } + } + + private void iLog(String action, String args){ + InteractionLog.log(INTERACTION_LOG_SOURCE,action,args); + } + + private Runnable unselectRunnable; + private Runnable frameBackupRunnable; + private static String INTERACTION_LOG_SOURCE = "HAPTIC"; + private EditorFrame frame; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/HapticTrigger.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,75 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionListener; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.haptics.HapticsFactory; + +/** + * A CollectionListener that updates the haptic scene upon insertion deletion and + * movement of nodes and edges in the diagram. + * + * + */ +public class HapticTrigger implements CollectionListener { + + @Override + public void elementInserted(CollectionEvent evt) { + if(evt.getDiagramElement() instanceof Node){ + Node n = (Node)evt.getDiagramElement(); + HapticsFactory.getInstance().addNode(n.getBounds().getCenterX(), n.getBounds().getCenterY(), System.identityHashCode(n)); + }else{//edge + Edge e = (Edge)evt.getDiagramElement(); + Edge.PointRepresentation pr = e.getPointRepresentation(); + HapticsFactory.getInstance().addEdge(System.identityHashCode(e),pr.xs,pr.ys,pr.adjMatrix,pr.nodeStart,e.getStipplePattern(),e.getNameLine()); + } + } + + @Override + public void elementTakenOut(CollectionEvent evt) { + if(evt.getDiagramElement() instanceof Node){ + Node n = (Node)evt.getDiagramElement(); + HapticsFactory.getInstance().removeNode(System.identityHashCode(n)); + }else{//edge + Edge e = (Edge)evt.getDiagramElement(); + HapticsFactory.getInstance().removeEdge(System.identityHashCode(e)); + } + } + + @Override + public void elementChanged(ElementChangedEvent evt) { + if("stop_move".equals(evt.getChangeType())){ + if(evt.getDiagramElement() instanceof Edge){ + Edge e = (Edge)evt.getDiagramElement(); + Edge.PointRepresentation pr = e.getPointRepresentation(); + HapticsFactory.getInstance().updateEdge(System.identityHashCode(e), pr.xs, pr.ys,pr.adjMatrix,pr.nodeStart, e.getNameLine()); + }else{ + Node n = (Node)evt.getDiagramElement(); + HapticsFactory.getInstance().moveNode( + n.getBounds().getCenterX(), + n.getBounds().getCenterY(), + System.identityHashCode(n) + ); + } + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/LineStyle.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,53 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.BasicStroke; +import java.awt.Stroke; + +/** + * Defines the possible values for the stroke of lines painted on the graph when drawing an {@link Edge}. + * It can be <i>Solid</i>, <i>Dotted</i> or <i>Dashed</i>. + * + */ +public enum LineStyle { + Solid(new BasicStroke()), + Dotted(new BasicStroke(1.0f, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 0.0f, + new float[]{1.0f,3.0f}, + 0.0f)), + Dashed(new BasicStroke(1.0f, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 0.0f, + new float[]{5.0f,5.0f}, + 0.0f)); + + private LineStyle(BasicStroke stroke){ + this.stroke = stroke; + } + + public Stroke getStroke(){ + return stroke; + } + + private Stroke stroke; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Lock.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,36 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +/** + * + * An enum that defines the possible locks that can be granted on a shared diagram. + * + */ +public enum Lock { + DELETE, + NAME, + PROPERTIES, + EDGE_END, + MOVE, + NOTES, + BOOKMARK, + MUST_EXIST, + NONE +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/LoopComboBox.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,89 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.event.ItemEvent; +import java.awt.event.KeyEvent; +import java.util.Vector; + +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; + +/** + * A ComboBox component which overrides the default behaviour when selecting items by the keyboard + * up and down arrow keys. When the top is reached, if the user presses the up arrow key + * instead of blocking on the first item, the LoopComboBox loops forward to the last item. Likewise, + * when the bottom is reached and the user presses the down arrow key the LoopComboBox + * loops back to the first item. + * + */ +@SuppressWarnings("serial") +public class LoopComboBox extends JComboBox { + public LoopComboBox(){ + super(); + } + + public LoopComboBox(Object[] items){ + super(items); + } + + public LoopComboBox(ComboBoxModel aModel){ + super(aModel); + } + + public LoopComboBox(Vector<?> items){ + super(items); + } + + @Override + public void processKeyEvent(KeyEvent e) { + if(dataModel.getSize() == 0){ + super.processKeyEvent(e); + }else{ + if(e.getKeyCode() == KeyEvent.VK_DOWN + && e.getID()==KeyEvent.KEY_PRESSED + && getSelectedIndex() == getItemCount()-1){ + setSelectedIndex(0); + if(getItemCount() == 1) + fireOneItemStateChanged(); + }else if(e.getKeyCode() == KeyEvent.VK_UP + && e.getID()==KeyEvent.KEY_PRESSED + && getSelectedIndex() == 0){ + setSelectedIndex(getItemCount()-1); + if(getItemCount() == 1) + fireOneItemStateChanged(); + }else + super.processKeyEvent(e); + } + } + + /** + * when the comboBox has only one item the ItemStateChanged listeners ain't fired by default. + * This behaviour has to be forced in order to have the item label to be spoken out by + * the narrator, in spite of the item number . + */ + private void fireOneItemStateChanged(){ + fireItemStateChanged(new ItemEvent( + this, + ItemEvent.ITEM_STATE_CHANGED, + getSelectedItem(), + ItemEvent.SELECTED + )); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/LoopSpinnerNumberModel.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,60 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import javax.swing.SpinnerNumberModel; + +/** + * A SpinnerNumberModel which overrides the default behaviour when selecting items by the keyboard + * up and down arrow keys. + * When the maximum value is reached, a call to getNextValue() + * will return the minimum value, instead of returning null. Likewise, + * when the minimum value is reached, a call to getPreviousValue() will return the maximum value + * instead of returning null. + * + * + */ +@SuppressWarnings("serial") +public class LoopSpinnerNumberModel extends SpinnerNumberModel { + public LoopSpinnerNumberModel(int value, int minimum, int maximum){ + super(value,minimum,maximum,1); + } + + @Override + public Object getNextValue(){ + Object nextValue = super.getNextValue(); + if(nextValue == null) + return getMinimum(); + else + return nextValue; + } + + @Override + public Object getPreviousValue(){ + Object previousValue = super.getPreviousValue(); + if(previousValue == null) + return getMaximum(); + else + return previousValue; + } + + public Object getValue(){ + return super.getValue(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/ModifierEditorDialog.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,138 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Frame; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; + +/** + * + * A dialog showing a list of checkboxes. By selecting the checkboxes the user can choose + * which modifiers are assigned to a property value. + * + */ +@SuppressWarnings("serial") +public class ModifierEditorDialog extends JDialog { + + private ModifierEditorDialog(JDialog parent, List<String> modifierTypes, Set<Integer> modifierIndexes){ + super(parent, resources.getString("dialog.modifier_editor.title"), true); + init(modifierTypes, modifierIndexes); + } + + private ModifierEditorDialog(Frame parent, List<String> modifierTypes, Set<Integer> modifierIndexes){ + super(parent, resources.getString("dialog.modifier_editor.title"), true); + init(modifierTypes, modifierIndexes); + } + + private void init(List<String> modifierTypes, Set<Integer> modifierIndexes){ + listenerManager = new ListenerManager(); + createComponents(); + + panel.setLayout(new GridBagLayout()); + + checkBoxes = new JCheckBox[modifierTypes.size()]; + GridBagUtilities gridBagUtils = new GridBagUtilities(); + int i = 0; + for(String modifierType : modifierTypes){ + panel.add(new JLabel(modifierType), gridBagUtils.label()); + checkBoxes[i] = new JCheckBox(); + if(modifierIndexes.contains(i)) + checkBoxes[i].setSelected(true); + panel.add(checkBoxes[i],gridBagUtils.field()); + i++; + } + + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + okButton.addActionListener(listenerManager); + cancelButton.addActionListener(listenerManager); + panel.add(buttonPanel,gridBagUtils.all()); + + setContentPane(panel); + setResizable(false); + pack(); + } + + public static Set<Integer> showDialog(JDialog parent, List<String> modifierTypes, Set<Integer> modifiers){ + ModifierEditorDialog.modifiers = modifiers; + dialog = new ModifierEditorDialog(parent, modifierTypes, modifiers); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + return ModifierEditorDialog.modifiers; + + } + + public static Set<Integer> showDialog(Frame parent, List<String> modifierTypes, Set<Integer> modifiers){ + ModifierEditorDialog.modifiers = modifiers; + dialog = new ModifierEditorDialog(parent, modifierTypes, modifiers); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + return ModifierEditorDialog.modifiers; + + } + + private void createComponents(){ + panel = new JPanel(); + buttonPanel = new JPanel(); + okButton = new JButton(resources.getString("dialog.ok_button")); + cancelButton = new JButton(resources.getString("dialog.cancel_button")); + } + + private JPanel panel; + private JPanel buttonPanel; + private JButton okButton; + private JButton cancelButton; + private JCheckBox[] checkBoxes; + private ListenerManager listenerManager; + + private static Set<Integer> modifiers; + private static ModifierEditorDialog dialog; + private static ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + + private class ListenerManager implements ActionListener { + @Override + public void actionPerformed(ActionEvent evt) { + Object source = evt.getSource(); + if(source.equals(okButton)){ + for(int i=0;i<checkBoxes.length;i++) + if(checkBoxes[i].isSelected()) + modifiers.add(i); + else + modifiers.remove(i); + dispose(); + }else if(source.equals(cancelButton)){ + dispose(); + } + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Node.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,338 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramEdge; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; + +/** + * An node in a graph. Node objects are used in a GraphPanel to render diagram nodes visually. + * Node objects are used in the tree representation of the diagram as well, as they're + * subclasses of {@link DiagramNode} + * + */ +@SuppressWarnings("serial") +public abstract class Node extends DiagramNode implements GraphElement{ + + public Node(String type, NodeProperties properties){ + super(type,properties); + internalNodes = new ArrayList<Node>(); + attachedEdges = new ArrayList<Edge>(); + externalNode = null; + } + + /* --- DiagramNode abstract methods implementation --- */ + @Override + public int getEdgesNum(){ + return attachedEdges.size(); + } + + @Override + public Edge getEdgeAt(int index){ + return attachedEdges.get(index); + } + + @Override + public boolean addEdge(DiagramEdge e){ + return attachedEdges.add((Edge)e); + } + + @Override + public boolean removeEdge(DiagramEdge e){ + return attachedEdges.remove((Edge)e); + } + + @Override + public Node getExternalNode(){ + return externalNode; + } + + @Override + public void setExternalNode(DiagramNode node){ + externalNode = (Node)node; + } + + @Override + public Node getInternalNodeAt(int i){ + return internalNodes.get(i); + } + + @Override + public int getInternalNodesNum(){ + return internalNodes.size(); + } + + @Override + public void addInternalNode(DiagramNode node){ + internalNodes.add((Node)node); + } + + @Override + public void removeInternalNode(DiagramNode node){ + if (node.getExternalNode() != this) + return; + internalNodes.remove(node); + node.setExternalNode(null); + } + + @Override + public void stopMove(){ + notifyChange(new ElementChangedEvent(this,this,"stop_move")); + /* edges can change as a result of nodes motion thus we call the method for all the edges + * of the node regardless what the mouse point is */ + for(int i = 0; i < getEdgesNum();i++){ + getEdgeAt(i).stopMove(); + } + } + + /** + * Translates the node by a given amount + * @param p the point we are translating from + * @param dx the amount to translate in the x-direction + * @param dy the amount to translate in the y-direction + */ + public void translate( Point2D p , double dx, double dy){ + translateImplementation( p, dx, dy); + notifyChange(new ElementChangedEvent(this, this, "translate")); + } + + protected void translateImplementation(Point2D p , double dx, double dy){ + for(int i=0; i< getInternalNodesNum();i++){ + getInternalNodeAt(i).translate(p, dx, dy); + } + } + + /** + * Tests whether the node contains a point. + * @param aPoint the point to test + * @return true if this node contains aPoint + */ + public abstract boolean contains(Point2D aPoint); + + /** + * Get the bounding rectangle of the shape of this node + * @return the bounding rectangle + */ + @Override + public abstract Rectangle2D getBounds(); + + @Override + public void startMove(Point2D p){ + /* useless, here just to comply with the GraphElement interface */ + } + + public abstract Point2D getConnectionPoint(Direction d); + + @Override + public void draw(Graphics2D g2){ + if(!"".equals(getNotes())){ + Rectangle2D bounds = getBounds(); + Color oldColor = g2.getColor(); + g2.setColor(GraphPanel.GRABBER_COLOR); + g2.fill(new Rectangle2D.Double(bounds.getX() - MARKER_SIZE / 2, bounds.getY() - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE)); + g2.setColor(oldColor); + } + } + + public abstract Shape getShape(); + + public void encode(Document doc, Element parent){ + parent.setAttribute(PersistenceManager.NAME,getName()); + + Element positionTag = doc.createElement(PersistenceManager.POSITION); + Rectangle2D bounds = getBounds(); + positionTag.setAttribute(PersistenceManager.X, String.valueOf(bounds.getX())); + positionTag.setAttribute(PersistenceManager.Y, String.valueOf(bounds.getY())); + parent.appendChild(positionTag); + + if(getProperties().isEmpty()) + return; + + Element propertiesTag = doc.createElement(PersistenceManager.PROPERTIES); + parent.appendChild(propertiesTag); + for(String type : getProperties().getTypes()){ + List<String> values = getProperties().getValues(type); + if(values.isEmpty()) + continue; + Element propertyTag = doc.createElement(PersistenceManager.PROPERTY); + propertiesTag.appendChild(propertyTag); + + Element typeTag = doc.createElement(PersistenceManager.TYPE); + typeTag.appendChild(doc.createTextNode(type)); + propertyTag.appendChild(typeTag); + + int index = 0; + for(String value : values){ + Element elementTag = doc.createElement(PersistenceManager.ELEMENT); + propertyTag.appendChild(elementTag); + + Element valueTag = doc.createElement(PersistenceManager.VALUE); + valueTag.appendChild(doc.createTextNode(value)); + elementTag.appendChild(valueTag); + + + Set<Integer> modifierIndexes = getProperties().getModifiers(type).getIndexes(index); + if(!modifierIndexes.isEmpty()){ + Element modifiersTag = doc.createElement(PersistenceManager.MODIFIERS); + StringBuilder builder = new StringBuilder(); + for(Integer i : modifierIndexes ) + builder.append(i).append(' '); + builder.deleteCharAt(builder.length()-1);//remove last space + modifiersTag.appendChild(doc.createTextNode(builder.toString())); + elementTag.appendChild(modifiersTag); + } + index++; + } + } + } + + public void decode(Document doc, Element nodeTag) throws IOException{ + setName(nodeTag.getAttribute(PersistenceManager.NAME)); + try{ + setId(Integer.parseInt(nodeTag.getAttribute(PersistenceManager.ID))); + }catch(NumberFormatException nfe){ + throw new IOException(); + } + + if(nodeTag.getElementsByTagName(PersistenceManager.POSITION).item(0) == null) + throw new IOException(); + Element positionTag = (Element)nodeTag.getElementsByTagName(PersistenceManager.POSITION).item(0); + double dx,dy; + try{ + dx = Double.parseDouble(positionTag.getAttribute(PersistenceManager.X)); + dy = Double.parseDouble(positionTag.getAttribute(PersistenceManager.Y)); + }catch(NumberFormatException nfe){ + throw new IOException(); + } + Rectangle2D bounds = getBounds(); + translate(new Point2D.Double(0,0), dx - bounds.getX(), dy - bounds.getY()); + + NodeList propList = nodeTag.getElementsByTagName(PersistenceManager.PROPERTY); + NodeProperties properties = getProperties(); + for(int j=0; j<propList.getLength();j++){ + Element propertyTag = (Element)propList.item(j); + + if(propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null) + throw new IOException(); + Element pTypeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0); + String propertyType = pTypeTag.getTextContent(); + + /* scan all the <Element> of the current <Property>*/ + NodeList elemValueList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT); + for(int h=0; h<elemValueList.getLength(); h++){ + + Element elemTag = (Element)elemValueList.item(h); + /* get the <value> */ + if(elemTag.getElementsByTagName(PersistenceManager.VALUE).item(0) == null) + throw new IOException(); + Element valueTag = (Element)elemTag.getElementsByTagName(PersistenceManager.VALUE).item(0); + String value = valueTag.getTextContent(); + + /* <modifiers>. need to go back on the prototypes because the content of <modifier> is a list */ + /* of int index pointing to the modifiers type, defined just in the prototypes */ + Element prototypesTag = (Element)doc.getElementsByTagName(PersistenceManager.PROTOTYPES).item(0); + Modifiers modifiers = null; + try { + modifiers = properties.getModifiers(propertyType); + }catch(IllegalArgumentException iae){ + throw new IOException(iae); + } + if(!modifiers.isNull()){ + Element modifiersTag = (Element)((Element)elemValueList.item(h)).getElementsByTagName(PersistenceManager.MODIFIERS).item(0); + if(modifiersTag != null){ //else there are no modifiers specified for this property value + Set<Integer> indexesToAdd = new LinkedHashSet<Integer>(); + String indexesString = modifiersTag.getTextContent(); + String[] indexes = indexesString.split(" "); + for(String s : indexes){ + try{ + int index = Integer.parseInt(s); + NodeList templatePropList = prototypesTag.getElementsByTagName(PersistenceManager.PROPERTY); + String modifiersType = null; + /* look at the property prototypes to see which modifier the index is referring to. * + * The index is in fact the id attribute of the <Modifier> tag in the prototypes section */ + for(int k=0; k<templatePropList.getLength();k++){ + Element prototypePropTag = (Element)templatePropList.item(k); + Element prototypePropTypeTag = (Element)prototypePropTag.getElementsByTagName(PersistenceManager.TYPE).item(0); + + if(propertyType.equals(prototypePropTypeTag.getTextContent())){ + NodeList proptotypeModifierList = prototypePropTag.getElementsByTagName(PersistenceManager.MODIFIER); + for(int m = 0 ; m<proptotypeModifierList.getLength();m++){ + if(index == Integer.parseInt(((Element)proptotypeModifierList.item(m)).getAttribute(PersistenceManager.ID))){ + Element modifierTypeTag = (Element)((Element)proptotypeModifierList.item(m)).getElementsByTagName(PersistenceManager.TYPE).item(0); + modifiersType = modifierTypeTag.getTextContent(); + } + } + } + } + if(modifiersType == null) // the index must point to a valid modifier's id + throw new IOException(); + indexesToAdd.add(Integer.valueOf(modifiers.getTypes().indexOf(modifiersType))); + }catch(NumberFormatException nfe){ + throw new IOException(nfe); + } + } + addProperty(propertyType, value);//whether propertyType actually exist in the prototypes has been already checked + setModifierIndexes(propertyType, h, indexesToAdd); + }else + addProperty(propertyType, value); + }else + addProperty(propertyType, value); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public Object clone(){ + Node clone = (Node)super.clone(); + clone.internalNodes = (ArrayList<Node>) internalNodes.clone(); + clone.externalNode = null; + clone.attachedEdges = (ArrayList<Edge>) attachedEdges.clone(); + return clone; + } + + protected ArrayList<Node> internalNodes; + protected Node externalNode; + protected ArrayList<Edge> attachedEdges; + + private final int MARKER_SIZE = 7; + protected static final Color SHADOW_COLOR = Color.LIGHT_GRAY; + public static final int SHADOW_GAP = 2; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/NodePopupMenu.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,112 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPopupMenu; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * A pop up menu displaying the possible operations to perform on an node on from a visual representation + * of a diagram. + * + */ +@SuppressWarnings("serial") +public class NodePopupMenu extends JPopupMenu { + public NodePopupMenu(Node node, Component parentComponent, DiagramModelUpdater modelUpdater){ + nodeRef = node; + this.modelUpdater = modelUpdater; + this.parentComponent = parentComponent; + addMenuItems(); + } + + + private void addMenuItems(){ + /* add set name menu item*/ + JMenuItem setNameMenuItem = new JMenuItem(resources.getString("menu.set_name")); + setNameMenuItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent e) { + if(!modelUpdater.getLock(nodeRef, Lock.NAME)){ + iLog("Could not get the lock on node for renaming",DiagramElement.toLogString(nodeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.name")); + return; + } + iLog("open rename node dialog",DiagramElement.toLogString(nodeRef)); + String name = JOptionPane.showInputDialog(parentComponent, MessageFormat.format(resources.getString("dialog.input.name"),nodeRef.getName()), nodeRef.getName()); + if(name == null) + iLog("cancel rename node dialog",DiagramElement.toLogString(nodeRef)); + else + /* node has been locked at selection time */ + modelUpdater.setName(nodeRef, name); + modelUpdater.yieldLock(nodeRef, Lock.NAME); + } + + }); + add(setNameMenuItem); + + /* if the node has no properties defined, then don't add the menu item */ + if(nodeRef.getProperties().isNull()) + return; + /* add set property menu item*/ + JMenuItem setPropertiesMenuItem = new JMenuItem(resources.getString("menu.set_properties")); + setPropertiesMenuItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent e) { + if(!modelUpdater.getLock(nodeRef, Lock.PROPERTIES)){ + iLog("Could not get the lock on node for properties",DiagramElement.toLogString(nodeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.properties")); + modelUpdater.yieldLock(nodeRef, Lock.PROPERTIES); + return; + } + iLog("open edit properties dialog",DiagramElement.toLogString(nodeRef)); + NodeProperties properties = PropertyEditorDialog.showDialog(JOptionPane.getFrameForComponent(parentComponent),nodeRef.getPropertiesCopy()); + if(properties == null){ // user clicked on cancel + iLog("cancel edit properties dialog",DiagramElement.toLogString(nodeRef)); + modelUpdater.yieldLock(nodeRef, Lock.PROPERTIES); + return; + } + if(!properties.isNull()) + modelUpdater.setProperties(nodeRef,properties); + modelUpdater.yieldLock(nodeRef, Lock.PROPERTIES); + } + }); + add(setPropertiesMenuItem); + } + + private void iLog(String action, String args){ + InteractionLog.log("GRAPH",action,args); + } + + public Node nodeRef; + private DiagramModelUpdater modelUpdater; + private Component parentComponent; + private static ResourceBundle resources = ResourceBundle.getBundle(NodePopupMenu.class.getName()); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/NodePopupMenu.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,8 @@ + + +menu.set_name=Set Name +menu.set_properties=Set Properties + +dialog.lock_failure.name=Node name is being edited by another user +dialog.lock_failure.properties=Node properties are being edited by another user +dialog.input.name=Renaming {0}, Enter new name \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/PropertyEditorDialog.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,195 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTable; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; + +/** + * A Dialog for editing the {@link NodeProperties} of a diagram node. + * + */ +@SuppressWarnings("serial") +public class PropertyEditorDialog extends JDialog { + + private PropertyEditorDialog(Frame parent){ + super(parent, resources.getString("dialog.property_editor.title") , true); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + + addWindowListener(new WindowAdapter(){ + @Override + public void windowClosing(WindowEvent event){ + result = null; + dispose(); + } + }); + + listenerManager = new ListenerManager(); + + createComponents(); + setContentPane(scrollPane); + + panel.setLayout(new GridBagLayout()); + + GridBagUtilities gridBagUtils = new GridBagUtilities(); + panel.add(topSeparator,gridBagUtils.all()); + + int i = 0; + for(String type : properties.getTypes()){ + PropertyPanel propertyPanel = new PropertyPanel(type, properties.getValues(type), properties.getModifiers(type)); + panel.add(propertyPanel,gridBagUtils.all()); + propertyPanels[i++] = propertyPanel; + } + + if(!properties.getTypes().isEmpty()) + panel.add(bottomSeparator, gridBagUtils.all()); + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + panel.add(buttonPanel, gridBagUtils.all()); + + okButton.addActionListener(listenerManager); + cancelButton.addActionListener(listenerManager); + pack(); + } + + public static NodeProperties showDialog(Frame parent, NodeProperties properties){ + if(properties == null) + throw new IllegalArgumentException(resources.getString("dialog.property_editor.error.property_null")); + PropertyEditorDialog.properties = properties; + dialog = new PropertyEditorDialog(parent); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + return result; + } + + private void createComponents(){ + panel = new JPanel(); + buttonPanel = new JPanel(); + propertyPanels = new PropertyPanel[PropertyEditorDialog.properties.getTypes().size()]; + scrollPane = new JScrollPane( + panel, + JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER + ); + bottomSeparator = new JSeparator(); + topSeparator = new JSeparator(); + okButton = new JButton(resources.getString("dialog.ok_button")); + cancelButton = new JButton(resources.getString("dialog.cancel_button")); + } + + private JPanel panel; + private JPanel buttonPanel; + private PropertyPanel[] propertyPanels; + private JScrollPane scrollPane; + private JSeparator topSeparator; + private JSeparator bottomSeparator; + private JButton okButton; + private JButton cancelButton; + private ListenerManager listenerManager; + + private static PropertyEditorDialog dialog; + private static NodeProperties properties; + private static NodeProperties result; + private static ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + + private class PropertyPanel extends JPanel{ + public PropertyPanel(String propertyType, List<String> values, final Modifiers modifiers ){ + setLayout(new BoxLayout(this,BoxLayout.Y_AXIS)); + model = new PropertyTableModel(propertyType, values, modifiers); + if(modifiers != null) + for(int i=0; i< values.size(); i++){ + model.setIndexesAt(i, modifiers.getIndexes(i)); + } + table = new JTable(model); + table.setPreferredScrollableViewportSize(new Dimension(250, 70)); + table.setFillsViewportHeight(true); + + add(new JScrollPane(table)); + /* we can edit modifiers only if one or more modifier types have been defined for this property type */ + if(!modifiers.isNull()){ + editModifiers = new JButton(resources.getString("dialog.property_editor.edit_modifiers_button")); + editModifiers.setAlignmentX(RIGHT_ALIGNMENT); + add(editModifiers); + editModifiers.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent arg0) { + int row = table.getSelectedRow(); + if((row == -1)||(row == model.getRowCount()-1)) + return; + Set<Integer> indexes; + + indexes = ModifierEditorDialog.showDialog(PropertyEditorDialog.this, modifiers.getTypes(), model.getIndexesAt(row)); + model.setIndexesAt(row, indexes); + } + }); + } + add(Box.createRigidArea(new Dimension(0,5))); + } + + JTable table; + PropertyTableModel model; + JButton editModifiers; + } + + private class ListenerManager implements ActionListener{ + @Override + public void actionPerformed(ActionEvent evt) { + Object source = evt.getSource(); + if(source.equals(okButton)){ + for(int i=0; i<propertyPanels.length;i++){ + PropertyTableModel model = propertyPanels[i].model; + properties.clear(model.getColumnName(0)); + for(int j=0; j< model.getRowCount();j++){ + String value = model.getValueAt(j, 0).toString(); + if(!value.equals("")){ + properties.addValue(model.getColumnName(0),value , model.getIndexesAt(j)); + } + } + } + result = properties; + dispose(); + }else{ + result = null; + dispose(); + } + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/PropertyTableModel.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,141 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import java.util.Set; + +import javax.swing.table.AbstractTableModel; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; + + +/** + * + * A table model containing the property values currently edited and the modifiers assigned to them + * in the form of an array of indexes pointing to the modifiers types + */ +@SuppressWarnings("serial") +public class PropertyTableModel extends AbstractTableModel { + + public PropertyTableModel(String propertyType, List<String> values, Modifiers modifiers ){ + data = new ArrayList<ModifierString>(); + for(int i = 0; i< values.size();i++){ + String value = values.get(i); + data.add(new ModifierString(value, (modifiers == null) ? null : modifiers.getIndexes(i))); + } + data.add(new ModifierString(null)); + columnName = propertyType; + } + + @Override + public int getColumnCount() { + return 1; + } + + @Override + public int getRowCount() { + return data.size(); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return data.get(rowIndex); + } + + @Override + public String getColumnName(int column){ + return columnName; + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex){ + /* we filled up the last row, create another one */ + if((rowIndex == data.size()-1)&&(!value.toString().equals(""))){ + data.add(new ModifierString(null)); + data.get(rowIndex).value = value.toString(); + fireTableRowsInserted(data.size()-1, data.size()-1); + fireTableCellUpdated(rowIndex,columnIndex); + }else if((rowIndex != data.size()-1)&&(value.toString().equals(""))){ + data.remove(rowIndex); + fireTableRowsDeleted(rowIndex,rowIndex); + }else { + data.get(rowIndex).value = value.toString(); + fireTableCellUpdated(rowIndex,columnIndex); + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex){ + return true; + } + + public Set<Integer> getIndexesAt(int row){ + return data.get(row).modifierIndexes; + } + + public void setIndexesAt(int row, Set<Integer> indexes){ + data.get(row).modifierIndexes = new HashSet<Integer>(); + data.get(row).modifierIndexes.addAll(indexes); + } + + public void setIndexesAt(int row, Integer[] indexes){ + data.get(row).modifierIndexes = new HashSet<Integer>(); + for(int i=0; i<indexes.length; i++) + data.get(row).modifierIndexes.add(indexes[i]); + } + + List<ModifierString> data; + String columnName; + + private class ModifierString { + ModifierString(String value, Set<Integer> s){ + this.value = value; + modifierIndexes = new HashSet<Integer>(); + if(s != null){ + modifierIndexes.addAll(s); + } + } + + ModifierString(Set<Integer> s){ + this("",s); + } + + @Override + public boolean equals(Object o){ + return value.equals(o); + } + + @Override + public int hashCode(){ + return value.hashCode(); + } + + @Override + public String toString(){ + return value; + } + + String value; + Set<Integer> modifierIndexes; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/ResourceFactory.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,200 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Image; +import java.awt.event.ActionListener; +import java.beans.EventHandler; +import java.net.URL; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.KeyStroke; + +/** + * A factory class for swing components creation support + * + */ +public class ResourceFactory{ + public ResourceFactory(ResourceBundle bundle){ + this.bundle = bundle; + } + + public JMenuBar createMenuBar(){ + return SpeechMenuFactory.getMenuBar(); + } + + public JMenuItem createMenuItem(String prefix, + Object target, String methodName){ + return createMenuItem(prefix, + (ActionListener) EventHandler.create( + ActionListener.class, target, methodName)); + } + + public JMenuItem createMenuItem(String prefix, + ActionListener listener){ + String text = bundle.getString(prefix + ".text"); + JMenuItem menuItem = SpeechMenuFactory.getMenuItem(text); + return configure(menuItem, prefix, listener); + } + + public JMenuItem createMenuItemFromLabel(String label, ActionListener listener){ + JMenuItem menuItem = SpeechMenuFactory.getMenuItem(label); + menuItem.addActionListener(listener); + return menuItem; + } + + public JMenuItem createCheckBoxMenuItem(String prefix, + ActionListener listener){ + String text = bundle.getString(prefix + ".text"); + JMenuItem menuItem = SpeechMenuFactory.getJCheckBoxMenuItem(text); + return configure(menuItem, prefix, listener); + } + + public JMenuItem configure(JMenuItem menuItem, + String prefix, ActionListener listener){ + menuItem.addActionListener(listener); + try{ + String mnemonic = bundle.getString(prefix + ".mnemonic"); + menuItem.setMnemonic(mnemonic.charAt(0)); + }catch (MissingResourceException exception){ + // ok not to set mnemonic + } + + try{ + String accelerator = bundle.getString(prefix + ".accelerator"); + menuItem.setAccelerator(KeyStroke.getKeyStroke(accelerator)); + }catch (MissingResourceException exception){ + // ok not to set accelerator + } + + try{ + String tooltip = bundle.getString(prefix + ".tooltip"); + menuItem.setToolTipText(tooltip); + }catch (MissingResourceException exception){ + // ok not to set tooltip + } + return menuItem; + } + + public JMenu createMenu(String prefix){ + String text = bundle.getString(prefix + ".text"); + JMenu menu = SpeechMenuFactory.getMenu(text); + try{ + String mnemonic = bundle.getString(prefix + ".mnemonic"); + menu.setMnemonic(mnemonic.charAt(0)); + }catch (MissingResourceException exception){ + // ok not to set mnemonic + } + + try{ + String tooltip = bundle.getString(prefix + ".tooltip"); + menu.setToolTipText(tooltip); + } + catch (MissingResourceException exception) + { + // ok not to set tooltip + } + return menu; + } + + public JButton createButton(String prefix) { + String text = bundle.getString(prefix + ".text"); + JButton button = new JButton(text); + try { + String mnemonic = bundle.getString(prefix + ".mnemonic"); + button.setMnemonic(mnemonic.charAt(0)); + } + catch (MissingResourceException exception) { + // ok not to set mnemonic + } + + try { + String tooltip = bundle.getString(prefix + ".tooltip"); + button.setToolTipText(tooltip); + } + catch (MissingResourceException exception) { + // ok not to set tooltip + } + return button; + } + + + public Action configureAction(String prefix, Action action) + { + try + { + String text = bundle.getString(prefix + ".text"); + action.putValue(Action.NAME, text); + } + catch (MissingResourceException exception) + { + // ok not to set name + } + + try + { + String mnemonic = bundle.getString(prefix + ".mnemonic"); + action.putValue(Action.MNEMONIC_KEY, new Integer(mnemonic.charAt(0))); + } + catch (MissingResourceException exception) + { + // ok not to set mnemonic + } + + try { + String accelerator = bundle.getString(prefix + ".accelerator"); + action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(accelerator)); + } + catch (MissingResourceException exception){ + // ok not to set accelerator + } + + try + { + String tooltip = bundle.getString(prefix + ".tooltip"); + action.putValue(Action.SHORT_DESCRIPTION, tooltip); + } + catch (MissingResourceException exception) + { + // ok not to set tooltip + } + return action; + } + + public static class ImageFactory{ + public Image getImage(String path){ + URL url = getClass().getResource(path); + if(url != null) + return new ImageIcon(url).getImage(); + else + return null; + } + } + + private ResourceBundle bundle; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechLogDialog.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,100 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Frame; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.ScrollPaneConstants; + +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; + +/** + * A log handler that displays log records on a JTextArea in a dedicated Frame and speaks them out + * through text to speech synthesis performed by the {@link Narrator} instance. + * + */ +public class SpeechLogDialog extends Handler { + public static SpeechLogDialog getSpeechLogDialog(Frame parent){ + /* the static reference prevent the handler from being garbage collected * + * as the logger has a static management */ + if(speechLogDialog == null) + speechLogDialog = new SpeechLogDialog(parent); + return speechLogDialog; + } + + private SpeechLogDialog(Frame parent){ + setLevel(Level.CONFIG); + + area = new JTextArea(ROWS,COLS); + area.setEditable(false); + area.setLineWrap(true); + area.addFocusListener(new FocusAdapter(){ + @Override + public void focusGained(FocusEvent e) { + NarratorFactory.getInstance().speak("Server window focused"); + } + }); + + frame = new JFrame("Server"); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.getContentPane().add(new JScrollPane(area,ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER)); + frame.pack(); + frame.setLocationRelativeTo(parent); + frame.setVisible(true); + } + + @Override + public void close() throws SecurityException { + frame.dispose(); + speechLogDialog = null; + } + + @Override + public void flush() {} + + @Override + public void publish(LogRecord log) { + String prefix = ""; + if(log.getLevel().equals(Level.WARNING)){ + prefix = WARNING_PREFIX; + } + if(log.getLevel().equals(Level.SEVERE)) + prefix = ERROR_PREFIX; + String message = prefix+log.getMessage()+'\n'; + NarratorFactory.getInstance().speakWholeText(message); + area.append(message); + } + + JFrame frame; + JTextArea area; + private static final int ROWS = 10; + private static final int COLS = 50; + private static final String WARNING_PREFIX = "WARNING: "; + private static final String ERROR_PREFIX = "ERROR: "; + private static SpeechLogDialog speechLogDialog; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechMenuFactory.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,184 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComponent; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.MenuElement; +import javax.swing.MenuSelectionManager; + +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; + +/** + * + * This class provides a version of JMenuItem, JCheckBox, and JMenu which emit an "error" sound when + * they're disabled and the user tries to use it by an accelerator + */ +@SuppressWarnings("serial") +public class SpeechMenuFactory extends JMenu { + + /* implements the singleton pattern and keeps a static reference to the menuBar used bu JMenuItems and JMenus */ + public static JMenuBar getMenuBar(){ + if(menuBar == null){ + menuBar = new JMenuBar(){ + @Override + public void processKeyEvent(KeyEvent e, MenuElement[] path, MenuSelectionManager manager) { + super.processKeyEvent(e,path,manager); + if(e.getKeyCode() == KeyEvent.VK_ESCAPE){ + NarratorFactory.getInstance().speak("Leaving Menu"); + } + } + }; + } + return menuBar; + } + + public static JMenuItem getMenuItem(String text){ + return new SpeechMenuItem(text); + } + + public static JMenu getMenu(String text){ + return new JMenu(text){ + @Override + public void menuSelectionChanged(boolean isIncluded){ + super.menuSelectionChanged(isIncluded); + if(isIncluded && !wasMouse){ + String menuType = " menu"; + if(getMenuBar().getComponentIndex(this) == -1){ + menuType = " sub menu"; + }; + NarratorFactory.getInstance().speak(getAccessibleContext().getAccessibleName()+menuType); + } + wasMouse = false; + } + + @Override + public void processMouseEvent(MouseEvent e){ + wasMouse = true; + super.processMouseEvent(e); + } + + private boolean wasMouse = false; + }; + } + + public static JCheckBoxMenuItem getJCheckBoxMenuItem(String text){ + return new JCheckBoxMenuItem(text){ + @Override + public void menuSelectionChanged(boolean isIncluded){ + super.menuSelectionChanged(isIncluded); + if(isIncluded && !wasMouse ){ + NarratorFactory.getInstance().speak(getAccessibleContext().getAccessibleName()); + } + wasMouse = false; + } + + @Override + public void processMouseEvent(MouseEvent e){ + wasMouse = true; + super.processMouseEvent(e); + } + + private boolean wasMouse = false; + }; + } + + /* this action is called when the user strokes a disabled menu accelerator */ + private static Action errorAction = new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent e) { + /* clear from any previously registered playerListener to get sound only */ + SoundFactory.getInstance().unsetPlayerListener(SoundEvent.ERROR); + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + }; + + private static final String ACCELERATOR = "accelerator"; + + /* + * this class implements a menu item which speaks out its label when + * selected with the keyboard (mouse hover will have no effect) + * it needs a reference to the menu bar it's in to implement a respond, with the error sound, to + * the accelerator even when it's disabled (normally no listeners are called otherwise) + * + */ + private static class SpeechMenuItem extends JMenuItem{ + @Override + public void processMouseEvent(MouseEvent e ){ + wasMouse = true; + super.processMouseEvent(e); + } + + public SpeechMenuItem(String text){ + super(text); + /* bind ACCELERATOR with the error action */ + getMenuBar().getActionMap().put(ACCELERATOR, errorAction); + wasMouse = false; + if(text.trim().endsWith("...")){ + /* replace the ... in the accessible name with the voice version of it */ + String accName = getAccessibleContext().getAccessibleName().replaceAll("...\\s*$", ""); + getAccessibleContext().setAccessibleName(accName + " dot dot dot"); + } + } + + @Override + public void menuSelectionChanged(boolean isIncluded){ + super.menuSelectionChanged(isIncluded); + if(isIncluded && !wasMouse){ + String disabled = isEnabled() ? "" : ", disabled"; + String accelerator = ""; + if(getAccelerator() != null){ + accelerator = ", control " + getAccelerator().toString().substring(getAccelerator().toString().lastIndexOf(' ')); + } + NarratorFactory.getInstance().speak(getAccessibleContext().getAccessibleName()+disabled+accelerator); + } + wasMouse = false; + } + + @Override + public void setEnabled(boolean b){ + super.setEnabled(b); + if(b == false){ + /* if the menu item gets disabled, then set up an action in the menuBar to respond to + * the accelerator key stroke, so that the user gets a feedback (error sound) + * even though the listeners don't get called (as the menu item is disabled) + */ + getMenuBar().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(getAccelerator(), ACCELERATOR); + }else{ + getMenuBar().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(getAccelerator(), "none"); + } + } + + private boolean wasMouse; + }; + + private static JMenuBar menuBar; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechOptionPane.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,380 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Frame; +import java.awt.GridLayout; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.text.MessageFormat; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SwingWorker; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.text.JTextComponent; + +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +/** + * + * TheSpeechOptionPane provides one-line calls to display accessible dialog boxes. Input by the user as well + * as focused components are spoken out through text to speech synthesis performed by the {@link Narrator} instance. + * + */ +public abstract class SpeechOptionPane { + + public static String showTextAreaDialog(Component parentComponent, String message, String initialSelectionValue){ + JTextArea textArea = new JTextArea(NOTES_TEXT_AREA_ROW_SIZE,NOTES_TEXT_AREA_COL_SIZE); + textArea.setText(initialSelectionValue); + NarratorFactory.getInstance().speak(message); + return textComponentDialog(parentComponent, message, textArea); + } + + public static String showInputDialog(Component parentComponent, String message, String initialSelectionValue){ + final JTextField textField = new JTextField(initialSelectionValue); + textField.selectAll(); + NarratorFactory.getInstance().speak(message); + return textComponentDialog(parentComponent, message, textField); + } + + private static String textComponentDialog(Component parentComponent, String message, final JTextComponent textComponent){ + Object componentToDisplay = textComponent; + if(textComponent instanceof JTextArea) + componentToDisplay = new JScrollPane(textComponent); + + Object[] displayObjects = { new JLabel(message), componentToDisplay }; + final JOptionPane optPane = new JOptionPane(); + optPane.setMessage(displayObjects); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(OK_CANCEL_OPTION); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + final JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.input")); + if(textComponent instanceof JTextArea) + dialog.setResizable(true); + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + textComponent.requestFocusInWindow(); + } + }); + + SpeechUtilities.changeTabListener(optPane,dialog); + textComponent.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + textComponent.setEditable(true); + // start the editing sound + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + + if(optPane.getValue() == null)//window closed + return null; + else if(((Integer)optPane.getValue()).intValue() == CANCEL_OPTION || ((Integer)optPane.getValue()).intValue() == CLOSED_OPTION)//pressed on cancel + return null; + else{ // pressed on OK + return textComponent.getText().trim(); + } + } + + public static String showInputDialog(Component parentComponent, String message){ + return showInputDialog(parentComponent, message, ""); + } + + public static Object showSelectionDialog(Component parentComponent, String message, Object[] options, Object initialValue){ + final LoopComboBox comboBox = new LoopComboBox(options); + comboBox.setSelectedItem(initialValue); + Object[] displayObjects = { new JLabel(message), comboBox }; + JOptionPane optPane = new JOptionPane(); + optPane.setMessage(displayObjects); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(OK_CANCEL_OPTION); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + NarratorFactory.getInstance().speak(message+", "+SpeechUtilities.getComponentSpeech(comboBox)); + final JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.select")); + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + comboBox.requestFocusInWindow(); + } + }); + + comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + + SpeechUtilities.changeTabListener(optPane,dialog); + // start the editing sound + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + if(optPane.getValue() == null)//window closed + return null; + else if(((Integer)optPane.getValue()).intValue() == CANCEL_OPTION || ((Integer)optPane.getValue()).intValue() == CLOSED_OPTION)//pressed on cancel )//pressed on cancel + return null; + else{ // pressed on OK + return comboBox.getSelectedItem(); + } + } + + public static Integer showNarratorRateDialog(Component parentComponent, String message, int value, int min, int max){ + NarratorFactory.getInstance().speak(message); + final JSpinner spinner = new JSpinner(new LoopSpinnerNumberModel(value,min,max)); + JFormattedTextField tf = ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField(); + tf.setEditable(false); + tf.setFocusable(false); + tf.setBackground(Color.white); + + Object[] displayObjects = { new JLabel(message), spinner}; + final JOptionPane optPane = new JOptionPane(); + optPane.setMessage(displayObjects); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(OK_CANCEL_OPTION); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + final JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.input")); + SpeechUtilities.changeTabListener(optPane,dialog); + + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + spinner.requestFocusInWindow(); + } + }); + spinner.addChangeListener(new ChangeListener(){ + @Override + public void stateChanged(ChangeEvent evt) { + JSpinner s = (JSpinner)(evt.getSource()); + NarratorFactory.getInstance().setRate((Integer)s.getValue()); + NarratorFactory.getInstance().speak(s.getValue().toString()); + } + }); + // start the editing sound + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + + /* set the speech rate back to the value passed as argument */ + NarratorFactory.getInstance().setRate(value); + if(optPane.getValue() == null)//window closed + return null; + else if(((Integer)optPane.getValue()).intValue() == CANCEL_OPTION || ((Integer)optPane.getValue()).intValue() == CLOSED_OPTION)//pressed on cancel + return null; + else{ // pressed on OK + return (Integer)spinner.getValue(); + } + } + + public static int showConfirmDialog(Component parentComponent, String message, int optionType){ + NarratorFactory.getInstance().speak(message); + JOptionPane optPane = new JOptionPane(); + optPane.setMessage(message); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(optionType); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.confirm")); + SpeechUtilities.changeTabListener(optPane,dialog); + + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + + if(optPane.getValue() == null)//window closed + return CANCEL_OPTION; + else + return ((Integer)optPane.getValue()).intValue(); + } + + public static void showMessageDialog(Component parentComponent, String message, int messageType){ + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("dialog.speech_option_pane.message"), message)); + JOptionPane optPane = new JOptionPane(); + optPane.setMessage(message); + optPane.setMessageType(messageType); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.confirm")); + SpeechUtilities.changeTabListener(optPane,dialog); + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + } + + public static void showMessageDialog(Component parentComponent, String message){ + showMessageDialog(parentComponent,message,ERROR_MESSAGE); + } + + public static <T,V> int showProgressDialog(Component parentComponent, String message,final ProgressDialogWorker<T,V> worker, int millisToDecideToPopup){ + JProgressBar progressBar = new JProgressBar(); + progressBar.setIndeterminate(true); + Object displayObjects[] = {message, progressBar}; + final JOptionPane optPane = new JOptionPane(displayObjects); + optPane.setOptionType(DEFAULT_OPTION); + optPane.setOptions(new Object[] {PROGRESS_DIALOG_CANCEL_OPTION}); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + final JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.download")); + SpeechUtilities.changeTabListener(optPane,dialog); + + worker.setDialog(dialog); + worker.execute(); + try { + Thread.sleep(millisToDecideToPopup); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); //should never happen + } + if(worker.isDone()) + return OK_OPTION; + + NarratorFactory.getInstance().speak(message); + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + if( optPane.getValue() == null || + optPane.getValue() == PROGRESS_DIALOG_CANCEL_OPTION || + Integer.valueOf(CLOSED_OPTION).equals(optPane.getValue())){ + worker.cancel(true); + return CANCEL_OPTION; + } + return OK_OPTION; + } + + public static Set<Integer> showModifiersDialog(Component parentComponent, String message, List<String> modifierTypes, Set<Integer> modifierIndexes){ + JOptionPane optPane = new JOptionPane(); + + JPanel checkBoxPanel = new JPanel(new GridLayout(0, 1)); + final JCheckBox[] checkBoxes = new JCheckBox[modifierTypes.size()]; + for(int i=0;i<checkBoxes.length;i++){ + checkBoxes[i] = new JCheckBox(modifierTypes.get(i)); + if(modifierIndexes.contains(i)) + checkBoxes[i].setSelected(true); + checkBoxPanel.add(checkBoxes[i]); + checkBoxes[i].addItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + } + NarratorFactory.getInstance().speak(message+" "+SpeechUtilities.getComponentSpeech(checkBoxes[0])); + + Object[] displayObjects = {new JLabel(message),checkBoxPanel}; + optPane.setMessage(displayObjects); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(OK_CANCEL_OPTION); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.modifiers")); + SpeechUtilities.changeTabListener(optPane,dialog); + + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + checkBoxes[0].requestFocusInWindow(); + } + }); + + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + + if(optPane.getValue() == null)//window closed + return null; + else if(((Integer)optPane.getValue()).intValue() == CANCEL_OPTION || ((Integer)optPane.getValue()).intValue() == CLOSED_OPTION)//pressed on cancel + return null; + else{ // pressed on OK + Set<Integer> returnSet = new LinkedHashSet<Integer>(); + for(int i=0;i<checkBoxes.length;i++) + if(checkBoxes[i].isSelected()) + returnSet.add(i); + return returnSet; + } + } + + public static Frame getFrameForComponent(Component parentComponent){ + return JOptionPane.getFrameForComponent(parentComponent); + } + + private static ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + private static final int NOTES_TEXT_AREA_COL_SIZE = 10; + private static final int NOTES_TEXT_AREA_ROW_SIZE = 10; + private static final String PROGRESS_DIALOG_CANCEL_OPTION = resources.getString("dialog.speech_option_pane.cancel"); + + + public static final int QUESTION_MESSAGE = JOptionPane.QUESTION_MESSAGE; + public static final int ERROR_MESSAGE = JOptionPane.ERROR_MESSAGE; + public static final int INFORMATION_MESSAGE = JOptionPane.INFORMATION_MESSAGE; + public static final int WARNING_MESSAGE = JOptionPane.WARNING_MESSAGE; + public static final int OK_CANCEL_OPTION = JOptionPane.OK_CANCEL_OPTION; + public static final int CANCEL_OPTION = JOptionPane.CANCEL_OPTION; + public static final int OK_OPTION = JOptionPane.OK_OPTION; + public static final int CLOSED_OPTION = JOptionPane.CLOSED_OPTION; + public static final int DEFAULT_OPTION = JOptionPane.DEFAULT_OPTION; + public static final int YES_NO_OPTION = JOptionPane.YES_NO_OPTION; + public static final int YES_OPTION = JOptionPane.YES_OPTION; + public static final int NO_OPTION = JOptionPane.NO_OPTION; + + + public static abstract class ProgressDialogWorker<T,V> extends SwingWorker<T,V> { + private void setDialog(JDialog dialog){ + this.dialog = dialog; + } + + @Override + protected void done() { + if(dialog != null) + dialog.dispose(); + } + + private JDialog dialog; + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechSummaryPane.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,116 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.KeyStroke; + +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +/** + * Abstract class with an one-line call to display a summary dialog. + * The summary text as well as focused components are spoken out through text to speech + * synthesis performed by the {@link Narrator} instance. + * A summary dialog has non editable text field and a button + * for confirmation only. + * + * + */ +public abstract class SpeechSummaryPane { + + public static int showDialog(Component parentComponent, String title, String text, int optionType, String[] options){ + if(optionType == OK_CANCEL_OPTION && options.length < 2) + throw new IllegalArgumentException("option type and opions number must be consistent"); + final JTextArea textArea = new JTextArea(); + textArea.setText(text); + NarratorFactory.getInstance().speak(title+". "+ text); + + JScrollPane componentToDisplay = new JScrollPane(textArea); + /* set the maximum size: if there is a lot of content yet it doesn't take the whole screen */ + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + + int editorWidth = (int)screenSize.getWidth() * 5 / 8; + int editorHeight = (int)screenSize.getHeight() * 5 / 8; + + Dimension currentSize = componentToDisplay.getPreferredSize(); + componentToDisplay.setPreferredSize(new Dimension( + Math.min(currentSize.width, editorWidth) , Math.min(currentSize.height, editorHeight))); + + Object[] displayObjects = { new JLabel(title), componentToDisplay }; + final JOptionPane optPane = new JOptionPane(); + optPane.setMessage(displayObjects); + optPane.setMessageType(JOptionPane.PLAIN_MESSAGE); + optPane.setOptionType(optionType); + /* set the options according to the option type */ + optPane.setOptions(options); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + final JDialog dialog = optPane.createDialog(parentComponent, ""); + dialog.setResizable(true); + + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + textArea.requestFocusInWindow(); + } + }); + + SpeechUtilities.changeTabListener(optPane,dialog); + /* the textArea is not editable, so tab key event must not be consumed so that it can be picked up by the focus manager */ + textArea.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,0), "none"); + textArea.addKeyListener(SpeechUtilities.getSpeechKeyListener(false)); + textArea.setEditable(false); + // start the editing sound + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + NarratorFactory.getInstance().shutUp(); + + if(optPane.getValue() == null)//window closed + return CANCEL; + else if(optPane.getValue().equals(options[OK]))// pressed on OK + return OK; + else //pressed on cancel + return CANCEL; + } + + public static final int OK = 0; + public static final int CANCEL = 1; + public static final int OK_CANCEL_OPTION = JOptionPane.OK_CANCEL_OPTION; + public static final int OK_OPTION = JOptionPane.OK_OPTION; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/TemplateEditor.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,38 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Frame; +import java.util.Collection; + +/** + * A Template editor is used to create new types of diagrams. Such prototypes diagrams will then be used + * to create diagram instances (the actual diagrams operated by the user) through clonation. + * when a diagram prototype is created, it's added in the File->new Diagram menu. + */ +public interface TemplateEditor { + public Diagram createNew(Frame frame, Collection<String> existingTemplates); + + public Diagram edit(Frame frame, Collection<String> existingTemplates, Diagram diagram); + + public String getLabelForNew(); + + public String getLabelForEdit(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileChooser.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,55 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.awt.Component; +import java.io.File; + +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; + +/** + * A Component implementing this interface provides the basic file chooser functionality needed to + * in the CCmI Editor. + * + * + * @see JFileChooser + */ +public interface FileChooser { + public void setSelectedFile(File file); + + public File getSelectedFile(); + + public void setFileFilter(FileFilter filter); + + public void resetChoosableFileFilters(); + + public void setCurrentDirectory(File dir); + + public File getCurrentDirectory(); + + public int showOpenDialog(Component parent); + + public int showSaveDialog(Component parent); + + int APPROVE_OPTION = JFileChooser.APPROVE_OPTION; + + int CANCEL_OPTION = JFileChooser.CANCEL_OPTION; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileChooserFactory.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,44 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import javax.swing.JFileChooser; + +/** + * + * A Factory class which creates instances of the {@link FileChooser} interface. + * + */ +public abstract class FileChooserFactory { + public static FileChooser getFileChooser(boolean accessible){ + if(accessible) + return new SpeechFileChooser(); + else + return new NormalFileChooser(); + } + + /** + * Adapter class to get JFileChooser to implement the FileChooser Interface + * + */ + @SuppressWarnings("serial") + private static class NormalFileChooser extends JFileChooser implements FileChooser{ + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileSystemTree.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,286 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.LinkedList; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.filechooser.FileFilter; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; + +/* + * + * A JTree displaying the content for the local file system. + * + * + */ +@SuppressWarnings("serial") +class FileSystemTree extends JTree { + FileSystemTree(FileFilter filter){ + super(new DefaultTreeModel(FileSystemTreeNode.getRootNode(filter))); + getAccessibleContext().setAccessibleName(ResourceBundle.getBundle(SpeechFileChooser.class.getName()).getString("tree.accessible_name")); + getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + setCellRenderer(new FileSystemTreeCellRendered(getCellRenderer())); + + setSelectionPath(new TreePath(getModel().getRoot())); + overwriteTreeKeystrokes(); + this.addTreeSelectionListener(new TreeSelectionListener(){ + @Override + public void valueChanged(TreeSelectionEvent evt) { + if(treeSelectionListenerGateOpen){ + FileSystemTreeNode treeNode = (FileSystemTreeNode)evt.getPath().getLastPathComponent(); + NarratorFactory.getInstance().speak(treeNode.spokenText()); + } + } + }); + } + + @Override + public void setSelectionPath(TreePath path){ + super.setSelectionPath(path); + scrollPathToVisible(path); + } + + public void setSelectionPath(File file){ + if(file == null) + return; + + /* make a file path: a list of file's each one representing a directory of file's path */ + LinkedList<File> filePath = new LinkedList<File>(); + filePath.add(file); + File parent = file.getParentFile(); + while(parent != null){ + filePath.add(0, parent); + parent = parent.getParentFile(); + } + /* make a TreePath out of the file path */ + FileSystemTreeNode currentNode = (FileSystemTreeNode)getModel().getRoot(); + TreePath treePath = new TreePath(currentNode); + for(File f : filePath){ + boolean found = false; + for(int i=0;i<currentNode.getChildCount();i++){ + if(currentNode.getChildAt(i).getFile().equals(f)){ + currentNode = currentNode.getChildAt(i); + treePath = treePath.pathByAddingChild(currentNode); + found = true; + break; + } + } + if(!found) + break; + } + treeSelectionListenerGateOpen = false; + setSelectionPath(treePath); + treeSelectionListenerGateOpen = true; + } + + public void applyFilter(FileFilter filter){ + FileSystemTreeNode selectedNode = (FileSystemTreeNode)getSelectionPath().getLastPathComponent(); + File file = selectedNode.getFile(); + treeSelectionListenerGateOpen = false; + ((DefaultTreeModel)getModel()).setRoot(FileSystemTreeNode.getRootNode(filter)); + treeSelectionListenerGateOpen = true; + setSelectionPath(file); + } + + @Override + protected void processMouseEvent(MouseEvent e){ + //do nothing as the tree does not have to be editable with mouse + } + + private void overwriteTreeKeystrokes() { + /* overwrite the keys. up and down arrow are overwritten so that it loops when the top and the */ + /* bottom are reached rather than getting stuck */ + + /* Overwrite keystrokes up,down,left,right arrows and space, shift, ctrl */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0),"down"); + getActionMap().put("down", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FileSystemTreeNode treeNode = (FileSystemTreeNode)getLastSelectedPathComponent(); + /* look if we've got a sibling node after (we are not at the bottom) */ + FileSystemTreeNode nextTreeNode = treeNode.getNextSibling(); + SoundEvent loop = null; + if(nextTreeNode == null){ + TreeNode parent = treeNode.getParent(); + if(parent == null) /* root node, just stay there */ + nextTreeNode = treeNode; + else /* loop = go to first child of own parent */ + nextTreeNode = (FileSystemTreeNode)parent.getChildAt(0); + loop = SoundEvent.LIST_BOTTOM_REACHED; + } + + final String speech = nextTreeNode.spokenText(); + treeSelectionListenerGateOpen = false; + setSelectionPath(new TreePath(nextTreeNode.getPath())); + treeSelectionListenerGateOpen = true; + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + NarratorFactory.getInstance().speak(speech); + } + }); + }}); + + /* Overwrite keystrokes up,down,left,right arrows and space, shift, ctrl */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0),"up"); + getActionMap().put("up", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FileSystemTreeNode treeNode = (FileSystemTreeNode)getLastSelectedPathComponent(); + /* look if we've got a sibling node after (we are not at the bottom) */ + FileSystemTreeNode peviousTreeNode = treeNode.getPreviousSibling(); + SoundEvent loop = null; + if(peviousTreeNode == null){ + TreeNode parent = treeNode.getParent(); + if(parent == null) /* root node, just stay there */ + peviousTreeNode = treeNode; + else /* loop = go to first child of own parent */ + peviousTreeNode = (FileSystemTreeNode)parent.getChildAt(parent.getChildCount()-1); + loop = SoundEvent.LIST_TOP_REACHED; + } + + final String speech = peviousTreeNode.spokenText(); + treeSelectionListenerGateOpen = false; + setSelectionPath(new TreePath(peviousTreeNode.getPath())); + treeSelectionListenerGateOpen = true; + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + NarratorFactory.getInstance().speak(speech); + } + }); + }}); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0),"left"); + getActionMap().put("left", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + TreeNode treeNode = (TreeNode)path.getLastPathComponent(); + final FileSystemTreeNode parent = (FileSystemTreeNode)treeNode.getParent(); + if(parent == null){/* root node */ + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + else{ + TreePath newPath = new TreePath(parent.getPath()); + treeSelectionListenerGateOpen = false; + setSelectionPath(newPath); + collapsePath(newPath); + treeSelectionListenerGateOpen = true; + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_COLLAPSE,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(parent.spokenText()); + } + }); + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0),"right"); + getActionMap().put("right", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + TreeNode treeNode = (TreeNode)path.getLastPathComponent(); + if(treeNode.isLeaf()){/* leaf node */ + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + else{ + expandPath(path); + final FileSystemTreeNode firstChild = (FileSystemTreeNode)treeNode.getChildAt(0); + treeSelectionListenerGateOpen = false; + setSelectionPath(new TreePath(firstChild.getPath())); + treeSelectionListenerGateOpen = true; + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_EXPAND,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(firstChild.spokenText()); + } + }); + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,0),"space"); + getActionMap().put("space",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + FileSystemTreeNode treeNode = (FileSystemTreeNode)path.getLastPathComponent(); + NarratorFactory.getInstance().speak(treeNode.toString()); + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,InputEvent.CTRL_DOWN_MASK), "ctrl_space"); + getActionMap().put("ctrl_space", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + FileSystemTreeNode treeNode = (FileSystemTreeNode)path.getLastPathComponent(); + NarratorFactory.getInstance().speak(treeNode.getFile().getPath()); + } + }); + /* make the tree ignore the page up and page down keys */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,0),"none"); + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,0),"none"); + } + private boolean treeSelectionListenerGateOpen; +} + +/** + * This class overwrites the default cell renderer in order to always render directories with a + * directory-icon regardless whether they are a leaf node or not. + * + */ +class FileSystemTreeCellRendered implements TreeCellRenderer { + FileSystemTreeCellRendered(TreeCellRenderer delegate){ + this.delegate = delegate; + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, + boolean expanded, boolean leaf, int row, boolean hasFocus) { + if(leaf && ((FileSystemTreeNode)value).getFile().isDirectory() ) + return delegate.getTreeCellRendererComponent(tree, value, selected, expanded, false, row, hasFocus); + return delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + } + + TreeCellRenderer delegate; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileSystemTreeCellRenderer.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,45 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.awt.Component; + +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellRenderer; + +/* + * The cell renderer used in the FileSystemTree class. It differs from the default renderer in + * that it renders leaf nodes as non-leaf nodes they represent an empty directory + * rather than a file. + * + */ +@SuppressWarnings("serial") +class FileSystemTreeCellRenderer extends DefaultTreeCellRenderer { + @Override + public Component getTreeCellRendererComponent(JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus){ + return super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileSystemTreeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,223 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.io.File; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.ResourceBundle; + +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileSystemView; +import javax.swing.tree.TreeNode; + +/* + * The tree nodes of a FileSystemTree. Each nodes represent either a directory or a file + * in the local file system where the CCmI Editor is executed. + * + */ +class FileSystemTreeNode implements TreeNode { + + public static FileSystemTreeNode getRootNode(FileFilter filter){ + return new FileSystemTreeNode(filter){ + @Override + public boolean getAllowsChildren(){ + return true; + } + @Override + public String toString(){ + return ResourceBundle.getBundle(SpeechFileChooser.class.getName()).getString("tree_root.label"); + } + }; + } + + private FileSystemTreeNode(FileFilter filter){ // constructor for root + fileSystemView = FileSystemView.getFileSystemView(); + parent = null; + this.filter = filter; + containedFiles = File.listRoots(); + + children = new FileSystemTreeNode[containedFiles.length]; + for(int i=0; i< children.length; i++){ + children[i] = new FileSystemTreeNode(this, containedFiles[i],filter); + children[i].isFileSystemRoot = true; + } + } + + private FileSystemTreeNode(TreeNode parent, File file, FileFilter filter){ + fileSystemView = FileSystemView.getFileSystemView(); + this.file = file; + this.parent = parent; + this.filter = filter; + + if(file.isDirectory()){ + containedFiles = file.listFiles(); + if(containedFiles == null ) + containedFiles = new File[0]; + ArrayList<File> fileList = new ArrayList<File>(containedFiles.length); + for(File f : containedFiles){ + if(f.isDirectory() || filter.accept(f)) + fileList.add(f); + } + Collections.sort(fileList,new FileSystemComparator()); + containedFiles = new File[fileList.size()]; + containedFiles = fileList.toArray(containedFiles); + }else{ + containedFiles = new File[0]; + } + children = new FileSystemTreeNode[containedFiles.length]; + } + + public FileSystemTreeNode getNextSibling(){ + if(parent == null) + return null; + int thisIndex = parent.getIndex(this); + int numChildren = parent.getChildCount(); + if(thisIndex == numChildren-1) + return null; + return (FileSystemTreeNode)parent.getChildAt(thisIndex+1); + } + + public FileSystemTreeNode getPreviousSibling(){ + if(parent == null) + return null; + int thisIndex = parent.getIndex(this); + if(thisIndex == 0) + return null; + return (FileSystemTreeNode)parent.getChildAt(thisIndex-1); + } + + public TreeNode[] getPath(){ + List<TreeNode> pathList = new LinkedList<TreeNode>(); + pathList.add(0, this); + TreeNode parent = getParent(); + while(parent != null){ + pathList.add(0,parent); + parent = parent.getParent(); + } + TreeNode[] path = new TreeNode[pathList.size()]; + return pathList.toArray(path); + } + + public File getFile(){ + return file; + } + + public String spokenText(){ + String fileType = "dir"; + if(file != null && file.isFile()) + fileType = "file"; + return MessageFormat.format( + ResourceBundle.getBundle(SpeechFileChooser.class.getName()).getString(fileType), + toString() + ); + } + + @Override + public String toString(){ + if(isFileSystemRoot){ + String name = fileSystemView.getSystemDisplayName(file); + if(name.isEmpty()) + name = file.toString(); + return name; + } + return file.getName(); + } + + /* --- TREE NODE INTEFACE IMPLEMENTATION --- */ + @Override + public Enumeration<FileSystemTreeNode> children() { + return new Enumeration<FileSystemTreeNode>(){ + @Override + public boolean hasMoreElements() { + return (index < children.length); + } + + @Override + public FileSystemTreeNode nextElement() { + return new FileSystemTreeNode(parent, containedFiles[index++],filter); + } + int index = 0; + }; + } + + @Override + public boolean getAllowsChildren() { + return file.isFile(); + } + + @Override + public FileSystemTreeNode getChildAt(int index) throws IndexOutOfBoundsException { + /* builds the children lazily */ + if(children[index] == null) + children[index] = new FileSystemTreeNode(this, containedFiles[index],filter); + return children[index]; + } + + @Override + public int getChildCount() { + return containedFiles.length; + } + + @Override + public int getIndex(TreeNode n) { + FileSystemTreeNode fileSystemTreeNode = (FileSystemTreeNode)n; + for(int i=0; i< containedFiles.length; i++){ + if(fileSystemTreeNode.file.equals(containedFiles[i])) + return i; + } + return -1; + } + + @Override + public TreeNode getParent() { + return parent; + } + + @Override + public boolean isLeaf() { + return (containedFiles.length == 0); + } + + private File[] containedFiles; + private File file; + private FileSystemTreeNode[] children; + private TreeNode parent; + private FileSystemView fileSystemView; + private FileFilter filter; + private boolean isFileSystemRoot; +} + +class FileSystemComparator implements Comparator<File>{ + @Override + public int compare(File f1, File f2) { + if(f1.isDirectory() && f2.isFile()) + return -1; + else if(f1.isFile() && f2.isDirectory()) + return 1; + else + return f1.compareTo(f2); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,300 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.awt.Component; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.InputEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.KeyEvent; +import java.io.File; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.KeyStroke; +import javax.swing.filechooser.FileFilter; + +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; +import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; + +/* + * An accessible file chooser. Users can browse the file system only using hearing as + * in the same fashion as they do when exploring a diagram. + * + */ +@SuppressWarnings("serial") +class SpeechFileChooser extends JPanel implements FileChooser { + SpeechFileChooser(){ + super(new GridBagLayout()); + initComponents(); + addComponents(); + /* Enter will entail a unique action, regardless the component that's focused */ + getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "closeDialog"); + getActionMap().put("closeDialog", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FileSystemTreeNode treeNode = (FileSystemTreeNode)tree.getSelectionPath().getLastPathComponent(); + if(treeNode.getFile().isDirectory()){ + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + if(isOpenFileDialog) + openButton.doClick(); + else + saveButton.doClick(); + } + }); + } + + private void addComponents() { + GridBagUtilities gridBag = new GridBagUtilities(); + JScrollPane scrollPane = new JScrollPane(tree); + add(scrollPane,gridBag.all()); + add(fileNameLabel,gridBag.label()); + add(fileNameTextField, gridBag.field()); + add(fileTypeLabel, gridBag.label()); + add(fileTypeComboBox, gridBag.field()); + } + + private void initComponents(){ + resources = ResourceBundle.getBundle(this.getClass().getName()); + /* file name components */ + fileNameLabel = new JLabel(resources.getString("dialog.file_chooser.file_name")); + fileNameTextField = new TreeSelectionTextField(); + fileNameTextField.setColumns(20); + fileNameLabel.setLabelFor(fileNameTextField); + fileNameTextField.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + fileTypeLabel = new JLabel(resources.getString("dialog.file_chooser.file_type")); + + /* file type components */ + Object [] items = {new AllFileFilter(resources.getString("all_file_filter.label"))}; + fileTypeComboBox = new JComboBox(items); + fileTypeLabel.setLabelFor(fileTypeComboBox); + + /* open, save cancel buttons, when pressed, will close the dialog and assign a value to the option pane */ + openButton = new JButton(resources.getString("open_button.label")); + saveButton = new JButton(resources.getString("save_button.label")); + cancelButton = new JButton(resources.getString("cancel_button.label")); + ActionListener buttonListener = new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + if(evt.getSource().equals(openButton)||evt.getSource().equals(saveButton)){ + if(fileNameTextField.getText().isEmpty()){ + NarratorFactory.getInstance().speak(resources.getString("dialog.error.no_file_name")); + return; + } + } + optPane.setValue(evt.getSource()); + dialog.dispose(); + } + }; + openButton.addActionListener(buttonListener); + cancelButton.addActionListener(buttonListener); + saveButton.addActionListener(buttonListener); + + /* set up the listener binding the tree with the file name and file type components */ + tree = new FileSystemTree((FileFilter)fileTypeComboBox.getSelectedItem()); + tree.addTreeSelectionListener(fileNameTextField); + fileTypeComboBox.addItemListener(new ItemListener(){ + @Override + public void itemStateChanged(ItemEvent evt) { + if(evt.getStateChange() == ItemEvent.SELECTED) + tree.applyFilter((FileFilter)evt.getItem()); + } + }); + } + + @Override + public void setSelectedFile(File file){ + selectedFile = file; + if(file == null) + return; + tree.setSelectionPath(file); + } + + @Override + public File getSelectedFile(){ + return selectedFile; + } + + @Override + public void setFileFilter(FileFilter filter){ + if(filter != null){ + FileFilter wrap = new WrapFileFilter(filter); + ((DefaultComboBoxModel)fileTypeComboBox.getModel()).insertElementAt(wrap, 0); + fileTypeComboBox.getModel().setSelectedItem(wrap); + } + } + + @Override + public void resetChoosableFileFilters(){ + if(fileTypeComboBox.getItemCount() == 2) + ((DefaultComboBoxModel)fileTypeComboBox.getModel()).removeElementAt(0); + } + + @Override + public void setCurrentDirectory(File dir){ + currentDir = dir; + if(dir == null) + return; + tree.setSelectionPath(dir); + } + + @Override + public File getCurrentDirectory(){ + return currentDir; + } + + @Override + public int showOpenDialog(Component parent){ + isOpenFileDialog = true; + FileSystemTreeNode treeNode = (FileSystemTreeNode)tree.getSelectionPath().getLastPathComponent(); + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString("dialog.open.message"), + treeNode.spokenText())); + return showDialog(parent); + } + + @Override + public int showSaveDialog(Component parent){ + isOpenFileDialog = false; + FileSystemTreeNode treeNode = (FileSystemTreeNode)tree.getSelectionPath().getLastPathComponent(); + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString("dialog.save.message"), + treeNode.spokenText())); + return showDialog(parent); + } + + private int showDialog(Component parent){ + optPane = new JOptionPane(); + optPane.setMessage(this); + optPane.setMessageType(JOptionPane.PLAIN_MESSAGE); + Object[] options = { + isOpenFileDialog ? openButton : saveButton, + cancelButton + }; + optPane.setOptions(options); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + dialog = optPane.createDialog(parent, resources.getString("dialog.open.title")); + SpeechUtilities.changeTabListener(optPane,dialog); + + /* add the speech listener just before showing up, otherwise it will talk when filters are added/removed */ + fileTypeComboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + fileTypeComboBox.removeItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + if((isOpenFileDialog && openButton.equals(optPane.getValue()))||(!isOpenFileDialog && saveButton.equals(optPane.getValue()))){ + FileSystemTreeNode treeNode = (FileSystemTreeNode)tree.getSelectionPath().getLastPathComponent(); + /* user has made his choice. The returned file will be the directory selected in the tree * + * or the parent directory of the selected file if a file is selected + the string entered in the JTextField */ + File directory = treeNode.getFile(); + if(!directory.isDirectory()) + directory = directory.getParentFile(); + + selectedFile = new File(directory,fileNameTextField.getText()); + return APPROVE_OPTION; + }else{ + return CANCEL_OPTION; + } + } + + private ResourceBundle resources; + + private JLabel fileNameLabel; + private TreeSelectionTextField fileNameTextField; + private JLabel fileTypeLabel; + private JComboBox fileTypeComboBox; + private FileSystemTree tree; + private File selectedFile; + private File currentDir; + private JButton openButton; + private JButton saveButton; + private JButton cancelButton; + private JOptionPane optPane; + private JDialog dialog; + boolean isOpenFileDialog; +} + +class AllFileFilter extends FileFilter { + AllFileFilter(String description){ + this.description = description; + } + + @Override + public boolean accept(File file) { + return true; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String toString(){ + return description; + } + String description; +} + +class WrapFileFilter extends FileFilter { + WrapFileFilter(FileFilter delegate){ + this.delegate = delegate; + } + + @Override + public boolean accept(File f) { + return delegate.accept(f); + } + + @Override + public String getDescription() { + return delegate.getDescription(); + } + + @Override + public String toString(){ + return delegate.getDescription(); + } + + FileFilter delegate; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,19 @@ + +dir={0} Directory +file={0} + +tree_root.label=File System +tree.accessible_name=File System + +open_button.label=Open +cancel_button.label=Cancel +save_button.label=Save + +dialog.open.title=Open File +dialog.save.title=Save File +dialog.open.message=Open File dialog. {0} selected +dialog.save.message=Save File dialog. {0} selected +dialog.file_chooser.file_name=File Name: +dialog.file_chooser.file_type=File Type: +dialog.error.no_file_name=Error: no file name entered +all_file_filter.label=All Files \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/TreeSelectionTextField.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,44 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import javax.swing.JTextField; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; + +/* + * This text field will display the file name that the user selects when + * browsing the tree with the keyboard arrow keys. + */ +@SuppressWarnings("serial") +class TreeSelectionTextField extends JTextField implements TreeSelectionListener { + + @Override + public void valueChanged(TreeSelectionEvent evt) { + TreePath path = evt.getPath(); + FileSystemTreeNode node = (FileSystemTreeNode)path.getLastPathComponent(); + + if(node.getFile() != null && node.getFile().isFile()) + setText(node.toString()); + else + setText(""); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/license.txt Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PersistenceManager.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,553 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.persistence; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import javax.swing.tree.TreeNode; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel; +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.utils.CharEscaper; + +/** + * The PersistanceManager provides methods saving and retrieving diagrams from an XML + * file. Both templates diagrams (prototypes from which actual diagram instances are created + * through cloning) and diagram instances can be saved to a file. + * + */ +public abstract class PersistenceManager { + /** + * Encodes a diagram template in a file in XML format + * + * @param diagram the diagram to be encoded + * @param file the file where the diagram is going to be encoded + * @throws IOException if there are any I/O problems with the file + */ + public static void encodeDiagramTemplate(Diagram diagram, File file) throws IOException{ + ResourceBundle resources = ResourceBundle.getBundle(PersistenceManager.class.getName()); + if(file.createNewFile() == false) + throw new IOException(resources.getString("dialog.error.file_exists")); + + DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = null; + try { + docBuilder = dbfac.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new IOException(resources.getString("dialog.error.problem.save"),e); + } + Document doc = docBuilder.newDocument(); + + Element root = doc.createElement(DIAGRAM); + doc.appendChild(root); + /* diagram name and prototypePersstenceDelegate */ + root.setAttribute(NAME, diagram.getName()); + root.setAttribute(PROTOTYPE_PERSISTENCE_DELEGATE, diagram.getPrototypePersistenceDelegate().getClass().getName()); + + writePrototypes(doc, root, diagram); + + //set up a transformer + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer trans = null; + try { + trans = transfac.newTransformer(); + } catch (TransformerConfigurationException tce) { + throw new IOException(resources.getString("dialog.error.problem.save"),tce); + } + trans.setOutputProperty(OutputKeys.INDENT, "yes"); + trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(2)); + + StreamResult result = new StreamResult(new BufferedWriter(new FileWriter(file))); + DOMSource source = new DOMSource(doc); + try { + trans.transform(source, result); + } catch (TransformerException te) { + throw new IOException(resources.getString("dialog.error.problem.save"),te); + } + } + + /** + * Decodes a diagram template from a file in XML format + * + * @param XMLFile the file to read the diagram from + * @throws IOException if there are any I/O problems with the file + */ + public static Diagram decodeDiagramTemplate(File XMLFile) throws IOException{ + ResourceBundle resources = ResourceBundle.getBundle(PersistenceManager.class.getName()); + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = null; + try { + dBuilder = dbFactory.newDocumentBuilder(); + } catch (ParserConfigurationException pce) { + throw new IOException(resources.getString("dialog.error.problem.open"),pce); + } + Document doc = null; + try { + doc = dBuilder.parse(XMLFile); + } catch (SAXException se) { + throw new IOException(resources.getString("dialog.error.problem.open"),se); + } + doc.getDocumentElement().normalize(); + + if(doc.getElementsByTagName(DIAGRAM).item(0) == null) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element root = (Element)doc.getElementsByTagName(DIAGRAM).item(0); + String diagramName = root.getAttribute(NAME); + if(diagramName.isEmpty()) + throw new IOException(resources.getString("dialog.error.malformed_file")); + String persistenceDelegateClassName = root.getAttribute(PROTOTYPE_PERSISTENCE_DELEGATE); + PrototypePersistenceDelegate persistenceDelegate = null; + try{ + Class<? extends PrototypePersistenceDelegate> c = Class.forName(persistenceDelegateClassName).asSubclass(PrototypePersistenceDelegate.class); + persistenceDelegate = c.newInstance(); + }catch(Exception e){ + throw new IOException(resources.getString("dialog.error.problem.open"),e); + } + + final List<Node> nList = readNodePrototypes(doc,persistenceDelegate); + final List<Edge> eList = readEdgePrototypes(doc,persistenceDelegate); + Node[] nArray = new Node[nList.size()]; + Edge[] eArray = new Edge[eList.size()]; + return Diagram.newInstance(diagramName,nList.toArray(nArray),eList.toArray(eArray),persistenceDelegate); + } + + /** + * Encodes a diagram instance into the given output stream. Using output stream + * instead of Writer as it's advised by the StreamResult API + * @see http://download.oracle.com/javase/6/docs/api/javax/xml/transform/stream/StreamResult.html#setOutputStream(java.io.OutputStream) + * + * @param diagram : the diagram to encode + * @param out : where the diagram will be encoded + * @throws IOException + */ + public static void encodeDiagramInstance(Diagram diagram, String newName, OutputStream out) throws IOException{ + ResourceBundle resources = ResourceBundle.getBundle(PersistenceManager.class.getName()); + DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = null; + try { + docBuilder = dbfac.newDocumentBuilder(); + } catch (ParserConfigurationException pce) { + throw new IOException(resources.getString("dialog.error.problem.save"),pce); + } + Document doc = docBuilder.newDocument(); + + Element root = doc.createElement(DIAGRAM); + root.setAttribute(NAME, (newName != null) ? newName : diagram.getName()); + root.setAttribute(PROTOTYPE_PERSISTENCE_DELEGATE, diagram.getPrototypePersistenceDelegate().getClass().getName()); + doc.appendChild(root); + + /* store bookmarks */ + Element bookmarksTag = doc.createElement(BOOKMARKS); + TreeModel<Node,Edge> treeModel = diagram.getTreeModel(); + for(String key : treeModel.getBookmarks()){ + Element bookmarkTag = doc.createElement(BOOKMARK); + bookmarkTag.setAttribute(KEY, key); + if(treeModel.getBookmarkedTreeNode(key).isRoot()) + bookmarkTag.setTextContent(ROOT_AS_STRING); + else + bookmarkTag.setTextContent(getTreeNodeAsString(treeModel.getBookmarkedTreeNode(key))); + bookmarksTag.appendChild(bookmarkTag); + } + if(bookmarksTag.hasChildNodes()) + root.appendChild(bookmarksTag); + + /* store notes */ + Element notesTag = doc.createElement(NOTES); + DiagramModelTreeNode treeRoot = (DiagramModelTreeNode)diagram.getTreeModel().getRoot(); + for( @SuppressWarnings("unchecked") + Enumeration<DiagramModelTreeNode> enumeration = treeRoot.depthFirstEnumeration(); enumeration.hasMoreElements();){ + DiagramModelTreeNode treeNode = enumeration.nextElement(); + if(!treeNode.getNotes().isEmpty()){ + Element noteTag = doc.createElement(NOTE); + Element treeNodeTag = doc.createElement(TREE_NODE); + if(treeNode.isRoot()) + treeNodeTag.setTextContent(ROOT_AS_STRING); + else + treeNodeTag.setTextContent(getTreeNodeAsString(treeNode)); + Element contentTag = doc.createElement(CONTENT); + contentTag.setTextContent(CharEscaper.replaceNewline(treeNode.getNotes())); + noteTag.appendChild(treeNodeTag); + noteTag.appendChild(contentTag); + notesTag.appendChild(noteTag); + } + } + + if(notesTag.hasChildNodes()) + root.appendChild(notesTag); + + writePrototypes(doc,root,diagram); + + Element components = doc.createElement(COMPONENTS); + root.appendChild(components); + + synchronized(diagram.getCollectionModel().getMonitor()){ + Collection<Node> nodes = diagram.getCollectionModel().getNodes(); + Collection<Edge> edges = diagram.getCollectionModel().getEdges(); + + /* store nodes */ + Element nodesTag = doc.createElement(NODES); + components.appendChild(nodesTag); + List<Node> nList = new ArrayList<Node>(nodes); + for(Node n : nList){ + Element nodeTag = doc.createElement(NODE); + nodeTag.setAttribute(ID, String.valueOf(n.getId())); + nodeTag.setAttribute(TYPE, n.getType()); + nodesTag.appendChild(nodeTag); + n.encode(doc, nodeTag); + } + + Element edgesTag = doc.createElement(EDGES); + components.appendChild(edgesTag); + for(Edge e : edges){ + Element edgeTag = doc.createElement(EDGE); + edgesTag.appendChild(edgeTag); + e.encode(doc,edgeTag,nList); + } + } + //set up a transformer + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer trans = null; + try { + trans = transfac.newTransformer(); + } catch (TransformerConfigurationException tec) { + throw new IOException(resources.getString("dialog.error.problem.save")); + } + trans.setOutputProperty(OutputKeys.INDENT, "yes"); + trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(2)); + + + StreamResult result = new StreamResult(out); + DOMSource source = new DOMSource(doc); + try { + trans.transform(source, result); + } catch (TransformerException te) { + throw new IOException(resources.getString("dialog.error.problem.save"),te); + } + if(newName != null) + diagram.setName(newName); + } + + public static void encodeDiagramInstance(Diagram diagram, OutputStream out) throws IOException{ + encodeDiagramInstance(diagram,null,out); + } + + public static Diagram decodeDiagramInstance(InputStream in) throws IOException { + ResourceBundle resources = ResourceBundle.getBundle(PersistenceManager.class.getName()); + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = null; + try { + dBuilder = dbFactory.newDocumentBuilder(); + } catch (ParserConfigurationException pce) { + throw new IOException(resources.getString("dialog.error.problem.open"),pce); + } + Document doc = null; + try { + doc = dBuilder.parse(in); + } catch (SAXException se) { + throw new IOException(resources.getString("dialog.error.problem.open"),se); + } + doc.getDocumentElement().normalize(); + + if(doc.getElementsByTagName(DIAGRAM).item(0) == null) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element root = (Element)doc.getElementsByTagName(DIAGRAM).item(0); + String diagramName = root.getAttribute(NAME); + if(diagramName.isEmpty()) + throw new IOException(resources.getString("dialog.error.malformed_file")); + String persistenceDelegateClassName = root.getAttribute(PROTOTYPE_PERSISTENCE_DELEGATE); + PrototypePersistenceDelegate persistenceDelegate = null; + try{ + Class<? extends PrototypePersistenceDelegate> c = Class.forName(persistenceDelegateClassName).asSubclass(PrototypePersistenceDelegate.class); + persistenceDelegate = c.newInstance(); + }catch(Exception e){ + throw new IOException(resources.getString("dialog.error.problem.open"),e); + } + + final List<Node> nList = readNodePrototypes(doc,persistenceDelegate); + final List<Edge> eList = readEdgePrototypes(doc,persistenceDelegate); + + final Node[] nodes = nList.toArray(new Node[nList.size()]); + final Edge[] edges = eList.toArray(new Edge[eList.size()]); + + Diagram diagram = Diagram.newInstance(diagramName, nodes,edges,persistenceDelegate); + CollectionModel<Node,Edge> collectionModel = diagram.getCollectionModel(); + TreeModel<Node,Edge> treeModel = diagram.getTreeModel(); + + Map<String,Node> nodesId = new LinkedHashMap<String,Node>(); + + if(doc.getElementsByTagName(COMPONENTS).item(0) == null) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element componentsTag = (Element)doc.getElementsByTagName(COMPONENTS).item(0); + NodeList componentsChildren = componentsTag.getChildNodes(); + Element nodesTag = null; + for(int i=0;i<componentsChildren.getLength();i++){ + if(NODES.equals(componentsChildren.item(i).getNodeName())) + nodesTag = (Element)componentsChildren.item(i); + } + + NodeList elemList = nodesTag.getElementsByTagName(NODE); + for(int i=0; i<elemList.getLength();i++){ + Element nodeTag = (Element)elemList.item(i); + String idAsString = nodeTag.getAttribute(ID); + String type = nodeTag.getAttribute(TYPE); + Node prototype = null; + for(Node n : nList) + if(n.getType().equals(type)){ + prototype = n; + break; + } + if(prototype == null) + throw new IOException( + MessageFormat.format( + resources.getString("dialog.error.node_type_not_present"), + type)); + Node node = (Node)prototype.clone(); + + nodesId.put(idAsString, node); + + try { + Long id = Long.valueOf(idAsString); + node.setId(id); + }catch(NumberFormatException nfe){ + throw new IOException(resources.getString("dialog.error.malformed_file"),nfe); + } + collectionModel.insert(node); + try{ + node.decode(doc, nodeTag); + }catch(IOException ioe){ // just give a message to the exception + throw new IOException(resources.getString("dialog.error.malformed_file"),ioe); + } + } + + Element edgesTag = null; + for(int i=0;i<componentsChildren.getLength();i++) + if(EDGES.equals(componentsChildren.item(i).getNodeName())) + edgesTag = (Element)componentsChildren.item(i); + + elemList = edgesTag.getElementsByTagName(EDGE); + for(int i=0; i<elemList.getLength();i++){ + Element edgeTag = (Element)elemList.item(i); + String type = edgeTag.getAttribute(TYPE); + + Edge prototype = null; + for(Edge e : eList) + if(e.getType().equals(type)){ + prototype = e; + break; + } + if(prototype == null) + throw new IOException(MessageFormat.format( + resources.getString("dialog.error.edge_type_not_present"), + type + )); + + Edge edge = (Edge)prototype.clone(); + + try{ + edge.decode(doc, edgeTag, nodesId); + }catch(IOException ioe){ + throw new IOException(resources.getString("dialog.error.malformed_file"),ioe); + } + collectionModel.insert(edge); + } + + /* retrieve bookmarks */ + NodeList bookmarkList = root.getElementsByTagName(BOOKMARK); + for(int i=0;i<bookmarkList.getLength();i++){ + Element bookmarkTag = (Element)bookmarkList.item(i); + String key = bookmarkTag.getAttribute(KEY); + if(key.isEmpty()) + throw new IOException(resources.getString("dialog.error.malformed_file")); + String path = bookmarkTag.getTextContent(); + DiagramModelTreeNode treeNode = getTreeNodeFromString(treeModel,path); + treeModel.putBookmark(key, treeNode); + } + + /* retrieve notes */ + NodeList noteList = root.getElementsByTagName(NOTE); + for(int i=0;i<noteList.getLength();i++){ + Element noteTag = (Element)noteList.item(i); + if(noteTag.getElementsByTagName(TREE_NODE).item(0) == null ) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element pathTag = (Element)noteTag.getElementsByTagName(TREE_NODE).item(0); + String path = pathTag.getTextContent(); + if(noteTag.getElementsByTagName(CONTENT).item(0) == null) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element contentTag = (Element)noteTag.getElementsByTagName(CONTENT).item(0); + String content = CharEscaper.restoreNewline(contentTag.getTextContent()); + DiagramModelTreeNode treeNode = getTreeNodeFromString(treeModel,path); + treeModel.setNotes(treeNode,content); + } + + /* normally nodes and edges should be saved in order, this is to prevent * + * a manual editing of the xml to affect the program logic */ + collectionModel.sort(); + /* we have to do this has the insertion in the model made it modified */ + collectionModel.setUnmodified(); + return diagram; + } + + private static void writePrototypes(Document doc, Element root, Diagram diagram){ + Node[] nodes = diagram.getNodePrototypes(); + Edge[] edges = diagram.getEdgePrototypes(); + Element components = doc.createElement(PROTOTYPES); + root.appendChild(components); + + PrototypePersistenceDelegate delegate = diagram.getPrototypePersistenceDelegate(); + for(Node n : nodes){ + Element nodeTag = doc.createElement(NODE); + components.appendChild(nodeTag); + delegate.encodeNodePrototype(doc, nodeTag, n); + } + + for(Edge e : edges){ + Element edgeTag = doc.createElement(EDGE); + components.appendChild(edgeTag); + delegate.encodeEdgePrototype(doc, edgeTag, e); + } + } + + private static List<Node> readNodePrototypes(Document doc, PrototypePersistenceDelegate delegate) throws IOException{ + if(doc.getElementsByTagName(PROTOTYPES).item(0) == null) + throw new IOException(ResourceBundle.getBundle(PersistenceManager.class.getName()).getString("dialog.error.malformed_file")); + Element prototypesTag = (Element)doc.getElementsByTagName(PROTOTYPES).item(0); + NodeList elemList = prototypesTag.getElementsByTagName(NODE); + final List<Node> nList = new ArrayList<Node>(elemList.getLength()); + for(int i=0; i<elemList.getLength();i++){ + Element element = (Element)elemList.item(i); + try{ + Node n = delegate.decodeNodePrototype(element); + nList.add(n); + }catch(IOException ioe){ // just set the message for the exception + throw new IOException(ResourceBundle.getBundle(PersistenceManager.class.getName()).getString("dialog.error.malformed_file"),ioe); + } + } + return nList; + } + + private static List<Edge> readEdgePrototypes(Document doc, PrototypePersistenceDelegate delegate) throws IOException{ + if(doc.getElementsByTagName(PROTOTYPES).item(0) == null) + throw new IOException(ResourceBundle.getBundle(PersistenceManager.class.getName()).getString("dialog.error.malformed_file")); + Element prototypesTag = (Element)doc.getElementsByTagName(PROTOTYPES).item(0); + NodeList elemList = prototypesTag.getElementsByTagName(EDGE); + final List<Edge> eList = new ArrayList<Edge>(elemList.getLength()); + for(int i=0; i<elemList.getLength();i++){ + Element element = (Element)elemList.item(i); + try{ + Edge e = delegate.decodeEdgePrototype(element); + eList.add(e); + }catch(IOException ioe){ + throw new IOException(ResourceBundle.getBundle(PersistenceManager.class.getName()).getString("dialog.error.malformed_file"),ioe); + } + } + return eList; + } + + private static String getTreeNodeAsString(DiagramModelTreeNode treeNode){ + TreeNode[] path = treeNode.getPath(); + StringBuilder builder = new StringBuilder(); + for(int i=0;i<path.length-1; i++) + builder.append(String.valueOf(path[i].getIndex(path[i+1]))).append(' '); + if(builder.toString().endsWith(" ")) + builder.deleteCharAt(builder.length()-1); + return builder.toString(); + + } + + private static DiagramModelTreeNode getTreeNodeFromString(TreeModel<Node,Edge> model, String path) throws IOException{ + DiagramModelTreeNode treeNode = (DiagramModelTreeNode)model.getRoot(); + if(ROOT_AS_STRING.equals(path)) + return treeNode; + String[] nodesAsString = path.split(" "); + + try { + for(String nodeAsString : nodesAsString) + treeNode = (DiagramModelTreeNode) treeNode.getChildAt(Integer.parseInt(nodeAsString)); + }catch(Exception e){ + throw new IOException(e); + } + return treeNode; + } + + public final static String NAME = "Name"; + public final static String DIAGRAM = "Diagram"; + public final static String PROTOTYPE_PERSISTENCE_DELEGATE = "PrototypeDelegate"; + public final static String COMPONENTS = "Components"; + public final static String PROTOTYPES = "Prototypes"; + public final static String NODE = "Node"; + public final static String NODES = "Nodes"; + public final static String EDGE = "Edge"; + public final static String EDGES = "Edges"; + public final static String POSITION = "Position"; + public final static String PROPERTIES = "Properties"; + public final static String PROPERTY = "Property"; + public final static String TYPE = "Type"; + public final static String VALUE = "Value"; + public final static String ELEMENT = "Element"; + public static final String LABEL = "Label"; + public final static String POINTS = "Points"; + public final static String POINT = "Point"; + public final static String ID = "id"; + public final static String NEIGHBOURS = "Neighbours"; + public static final String MODIFIER = "Modifier"; + public static final String MODIFIERS = "Modifiers"; + public static final String X = "x"; + public static final String Y = "y"; + public static final String BOOKMARKS = "Bookmarks"; + public static final String BOOKMARK = "Bookmark"; + public static final String KEY = "Key"; + public static final String NOTES = "Notes"; + public static final String NOTE = "Note"; + public static final String CONTENT = "Content"; + public static final String TREE_NODE = "TreeNode"; + private static final String ROOT_AS_STRING = "-1"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PersistenceManager.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,8 @@ + + +dialog.error.file_exists=File already exist +dialog.error.node_type_not_present=Node type {0} not present in template definition +dialog.error.edge_type_not_present=Edge type {0} not present in template definition +dialog.error.problem.save=Error: a problem occurred while saving the file +dialog.error.problem.open=Error: a problem occurred while opening the file +dialog.error.malformed_file=Error: the opened file is malformed \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PrototypePersistenceDelegate.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,41 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.persistence; + +import java.io.IOException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.Edge; + +/** + * + * Each package providing an implementation of nodes and edges must provide a PrototypePersistenceDelegate + * as well. This class will be used by the PersistanceManager to save and retrieve the information necessary to rebuild + * nodes and edges from an XML file. + */ +public interface PrototypePersistenceDelegate { + public void encodeNodePrototype(Document doc, Element parent, Node n); + public void encodeEdgePrototype(Document doc, Element parent, Edge e); + public Node decodeNodePrototype(Element root) throws IOException; + public Edge decodeEdgePrototype(Element root) throws IOException; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/DummyHaptics.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,78 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.awt.geom.Line2D; +import java.util.BitSet; + +/* + * A dummy implementation of the Haptics interface. All its methods are empty, + * so every call will have no effect whatsoever. + */ +class DummyHaptics implements Haptics { + + public DummyHaptics(){} + + @Override + public int init(int width, int height) { return 0;} + + @Override + public synchronized void addNode(double x, double y, int diagramId) {} + + @Override + public synchronized void removeNode(int diagramId) {} + + @Override + public synchronized void removeEdge(int diagramId){} + + @Override + public synchronized void dispose() {} + + @Override + public void addNewDiagram(int id, boolean switchAfter) {} + + @Override + public synchronized void switchDiagram(int id) { } + + @Override + public synchronized void removeDiagram(int idToRemove, Integer idNext) {} + + @Override + public synchronized void moveNode(double x, double y, int diagramId) {} + + @Override + public synchronized void addEdge(int diagramId, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, int stipplePattern, Line2D attractLine) {} + + @Override + public synchronized void updateEdge(int diagramId, double[] xs, + double[] ys, BitSet[] adjMatrix, int nodeStart, Line2D attractLine) {} + + @Override + public synchronized void attractTo(int diagramId) {} + + @Override + public boolean isAlive(){ + return false; + } + + @Override + public void run() {} +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Edge.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,54 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.util.BitSet; + +/* + * A diagram edge in the haptics space. + */ +class Edge { + + public Edge(int diagramId, int hapticId, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart , int stipplePattern, double attractPointX, double attractPointY ) { + assert(xs.length == ys.length); + this.size = xs.length; + this.diagramId = diagramId; + this.hapticId = hapticId; + this.xs = xs; + this.ys = ys; + this.adjMatrix = adjMatrix; + this.stipplePattern = stipplePattern; + this.attractPointX = attractPointX; + this.attractPointY = attractPointY; + this.nodeStart = nodeStart; + } + + public double xs[] ; + public double ys[] ; + public int size; + public BitSet adjMatrix[]; + public int diagramId; + public int hapticId; + public int stipplePattern; + public double attractPointX; + public double attractPointY; + public int nodeStart; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListener.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,69 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +/** + * + * An HapticListeners is a thread listening to commands sent by an haptic device through + * shared memory and executes them. The piece of software that manages the haptic device + * runs on its own thread, hence the need for a inter-thread communication. Listening + * to the haptic device cannot be done by the event dispatching thread as this + * would prevent the user from using the graphical user interface, therefore a further thread is needed + * for this task. + * HapticListener is an abstract class which must be extended by implementing the + * {@link #executeCommand(HapticListenerCommand, int, double, double, double, double)} method. + * + */ +public abstract class HapticListener extends Thread { + + public HapticListener() { + super("Haptic Listener"); + mustSayGoodBye = false; + } + + @Override + public final void run(){ + synchronized(this){ + while(!mustSayGoodBye){ + try { + wait(); + executeCommand(HapticListenerCommand.fromChar(cmd), diagramElementID, x, y, startX, startY); + notify(); // notify the command has been executed + } catch (InterruptedException e) { + dispose(); + } + } + } + } + + public abstract void executeCommand(HapticListenerCommand cmd, int ID, double x, double y, double startX, double startY); + + public void dispose(){ + mustSayGoodBye = true; + } + + private char cmd; + private int diagramElementID; + private boolean mustSayGoodBye; + private double x; + private double y; + private double startX; + private double startY; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListenerCommand.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,71 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +/** + * enum of commands that an haptic listener can receive by an haptic device. + * + */ +public enum HapticListenerCommand { + PLAY_ELEMENT_SOUND, + PLAY_ELEMENT_SPEECH, + PLAY_SOUND, + SELECT, + UNSELECT, + MOVE, + INFO, + NONE, + ERROR; + + public static HapticListenerCommand fromChar(char c){ + switch(c){ + case 'p' : return PLAY_ELEMENT_SOUND; + case 't' : return PLAY_ELEMENT_SPEECH; + case 's' : return SELECT; + case 'm' : return MOVE; + case 'i' : return INFO; + case 'u' : return UNSELECT; + case 'g' : return PLAY_SOUND; + case 'e' : return ERROR; + default : return NONE; + } + } + + public enum Sound { + NONE, + MAGNET_OFF, + MAGNET_ON, + HOOK_ON, + DRAG; + + public static Sound fromInt(int i){ + switch(i){ + case 0 : return MAGNET_OFF; + case 1 : return MAGNET_ON; + case 2 : return HOOK_ON; + case 3 : return DRAG; + default : return NONE; + } + + } + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Haptics.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,57 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.awt.geom.Line2D; +import java.io.IOException; +import java.util.BitSet; + +public interface Haptics extends Runnable{ + + public int init(int width, int height) throws IOException; + + public void addNewDiagram(int id, boolean switchAfter); + + public void switchDiagram(int id); + + public void removeDiagram(int idToRemove, Integer idNext); + + public void addNode(double x, double y, int diagramId); + + public void removeNode(int diagramId); + + public void moveNode(double x, double y, int diagramId); + + public void addEdge(int diagramId, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, int stipplePattern, + Line2D attractLine); + + public void updateEdge(int diagramId, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, Line2D attractLine); + + public void removeEdge(int diagramId); + + public void attractTo(int diagramId); + + public boolean isAlive(); + + public void dispose(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticsFactory.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,44 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +/** + * + * Creates an instance of a class implementing the Haptics interface. There can only be one instance of such + * class. Therefore the factory uses the singleton pattern to always return the same object + * after it is created. + * + */ +public class HapticsFactory { + public static void createInstance(HapticListener listener) { + if(hapticsInstance != null) + throw new IllegalStateException("create instance must be called once only"); + hapticsInstance = new DummyHaptics(); + } + + public static Haptics getInstance(){ + if(hapticsInstance == null){ + throw new IllegalStateException("static method createInstance() must be called before getInstance()"); + } + return hapticsInstance; + } + + private static Haptics hapticsInstance; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Node.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,50 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.util.ArrayList; + +/* + * + * A diagram node in the haptics space. + * + */ +class Node { + public Node(double x, double y, int diagramId, int hapticId){ + this.x = x; + this.y = y; + this.diagramId = diagramId; + this.hapticId = hapticId; + edges = new ArrayList<Edge>(5); + } + + + public double x; + public double y; + /** + * the id on the diagram "id space". it corresponds to the hash code of the + * diagram nodes + * @see : uk.ac.eecs.qmul.ccmi.components.Node + */ + public int diagramId; // not shared with the haptic thread + public int hapticId; + public ArrayList<Edge> edges; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,182 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.main; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ResourceBundle; + +import javax.swing.SwingUtilities; + +import uk.ac.qmul.eecs.ccmi.gui.EditorFrame; +import uk.ac.qmul.eecs.ccmi.gui.HapticKindle; +import uk.ac.qmul.eecs.ccmi.gui.TemplateEditor; +import uk.ac.qmul.eecs.ccmi.haptics.Haptics; +import uk.ac.qmul.eecs.ccmi.haptics.HapticsFactory; +import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleTemplateEditor; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.CCmIUncaughtExceptionHandler; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/** + * + * The application class with the main method. The main performs + * the start up initialization and then displays the graphical user interface. + * + */ +public class DiagramEditorApp implements Runnable { + + /** + * perform initialization prior to displaying the GUI + */ + public void init(String[] args) { + Thread.setDefaultUncaughtExceptionHandler(new CCmIUncaughtExceptionHandler()); + final ResourceBundle resources = ResourceBundle.getBundle(this.getClass().getName()); + /* read command line arguments */ + if(args.length > 1){ + System.out.println(resources.getString("usage")); + System.exit(-1); + } + boolean enableLog = false; + if(args.length == 1){ + if(args[0].equals("-l")){ + enableLog = true; + System.out.println("log enabled"); + }else{ + System.out.println(resources.getString("usage")); + System.exit(-1); + } + } + + /* create the home directory if it does not exist and store the path into the preferences */ + PreferencesService preferences = PreferencesService.getInstance(); + String homeDirPath = preferences.get("home", null); + if(homeDirPath == null){ + homeDirPath = new StringBuilder(System.getProperty("user.home")).append(System.getProperty("file.separator")).append(resources.getString("dir.home")).toString(); + preferences.put("home", homeDirPath); + } + File homeDir = new File(homeDirPath); + homeDir.mkdir(); + + File backupDir = new File(homeDir,resources.getString("dir.backups")); + backupDir.mkdir(); + backupDirPath = backupDir.getAbsolutePath(); + + /* create the templates directory into the home directory */ + File templateDir = new File(homeDir,resources.getString("dir.templates")); + templateDir.mkdir(); + + /* create the images directory into the home directory */ + File imagesDir = new File(homeDir,resources.getString("dir.images")); + if(imagesDir.mkdir()) + preferences.put("dir.images", imagesDir.getAbsolutePath()); + + /* create the diagrams dir into the home directory */ + File diagramDir = new File(homeDir,resources.getString("dir.diagrams")); + if(diagramDir.mkdir()) + preferences.put("dir.diagrams", diagramDir.getAbsolutePath()); + + /* create the libs directory into he home directory */ + File libsDir = new File(homeDir,resources.getString("dir.libs")); + if(libsDir.mkdir()) + preferences.put("dir.libs", libsDir.getAbsolutePath()); + + /* read the template files into an array to pass to the EditorFrame instance */ + if(templateDir.exists()){ + FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(File f, String name) { + return (name.endsWith(resources.getString("template.extension"))); + } + }; + templateFiles = templateDir.listFiles(filter); + }else{ + templateFiles = new File[0]; + } + + if(enableLog){ + File logDir = new File(homeDir,resources.getString("dir.log")); + logDir.mkdir(); + try{ + InteractionLog.enable(logDir.getAbsolutePath()); + InteractionLog.log("PROGRAM STARTED"); + }catch(IOException ioe){ + /* if logging was enabled, the possibility to log is considered inescapable */ + /* do not allow the execution to continue any further */ + throw new RuntimeException(ioe); + } + } + + NarratorFactory.createInstance(); + SoundFactory.createInstance(); + + hapticKindle = new HapticKindle(); + HapticsFactory.createInstance(hapticKindle); + haptics = HapticsFactory.getInstance(); + if(!haptics.isAlive()){ + hapticKindle = null; + } + } + + /** + * build up the GUI and display it + */ + @Override + public void run() { + editorFrame = new EditorFrame(haptics,templateFiles,backupDirPath,getTemplateEditors()); + if(hapticKindle != null) + hapticKindle.setEditorFrame(editorFrame); + } + + public TemplateEditor[] getTemplateEditors(){ + TemplateEditor[] templateEditors = new TemplateEditor[1]; + templateEditors[0] = new SimpleTemplateEditor(); + return templateEditors; + } + + /** + * @param args + */ + public static void main(String[] args) { + DiagramEditorApp application = new DiagramEditorApp(); + + application.init(args); + + try { + SwingUtilities.invokeAndWait(application); + } catch (InvocationTargetException ex) { + throw new RuntimeException(ex); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + EditorFrame editorFrame; + HapticKindle hapticKindle; + Haptics haptics; + File[] templateFiles; + TemplateEditor[] templateCreators; + String backupDirPath; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,22 @@ + +template.extension=.xml +dir.home=ccmi_editor_data +dir.templates=templates +dir.diagrams=diagrams +dir.backups=backups +dir.images=images +dir.libs=libs +dir.log=log +usage=Unrecognized option(s)\nUsage : java -jar ccmi.jar [-l] \n -l : enables interaction log + + +#### APPLICATION PREFERENCES #### +# server.local_port +# server.remote_port +# home +# dir.diagrams +# dir.images +# dir.libs +# laf +# recent +# use_accessible_filechooser
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/CCmIOSCBundle.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,53 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + + +import de.sciss.net.OSCBundle; + +/** + * an OSCBundle implementation for the CCmI Editor. It overrides the behaviour of + * getTimeTag() and setTimeTagAbsMillis(long when) in order to provide a + * time tag representation more suitable for the interaction logging. + * + */ +class CCmIOSCBundle extends OSCBundle { + public CCmIOSCBundle(long timestamp){ + this.timestamp = timestamp; + } + + public CCmIOSCBundle(){ + this(System.currentTimeMillis()); + } + + @Override + public long getTimeTag(){ + return timestamp; + } + + @Override + public void setTimeTagAbsMillis(long when) { + super.setTimeTagAbsMillis(when); + this.timestamp = when; + } + + private long timestamp; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/CCmIOSCPacketCodec.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,56 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import de.sciss.net.OSCPacketCodec; + +/* + * an OSC OSCPacketCodec with MODE_FAT_V1 support mode. + * + * @see OSCPacketCodec#MODE_FAT_V1 + */ +class CCmIOSCPacketCodec extends OSCPacketCodec { + + public CCmIOSCPacketCodec(){ + super(OSCPacketCodec.MODE_FAT_V1); + } + + /*@Override + protected CCmIOSCBundle decodeBundle(ByteBuffer b) throws IOException{ + OSCBundle oldFashion = super.decodeBundle(b); + CCmIOSCBundle newFashion = new CCmIOSCBundle(); + b.position("#bundle\0".length()); + long timestamp = b.getLong(); + newFashion.setTimeTagAbsMillis(timestamp); + for(int i = 0; i<oldFashion.getPacketCount();i++){ + newFashion.addPacket(oldFashion.getPacket(i)); + } + return newFashion; + } + + @Override + protected void encodeBundle(OSCBundle bndl, ByteBuffer b) throws IOException{ + super.encodeBundle(bndl, b); + super.encodeBundle(bndl, b); + b.position("#bundle\0".length()); + long timestamp = b.getLong(); + System.out.println("time in encode b" + timestamp); + }*/ +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ClientConnectionManager.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,695 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import javax.swing.SwingUtilities; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.DiagramPanel; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.EditorTabbedPane; +import uk.ac.qmul.eecs.ccmi.gui.Finder; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.SpeechOptionPane; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * This is the class that manages the connection with the server. When a diagram is shared + * this class becomes responsible for actually operating the model (trough the EVT tough, by calling SwingUtilities.invokeLater ). + * If the operation is issued by the local user, than it performs the local action with local data only after + * being acknowledged by the server, else it creates the data on demand. For example upon an insert + * issued by the server, the element is created from scratch, according to the message of the server. + * + */ +public class ClientConnectionManager extends Thread { + + public ClientConnectionManager(EditorTabbedPane tabbedPane) throws IOException{ + super("Client Connection Manager"); + channels = new HashMap<SocketChannel, Diagram>(); + requests = new ConcurrentLinkedQueue<Request>(); + answers = new LinkedBlockingQueue<Answer>(); + pendingCommands = new LinkedList<SendCmdRequest>(); + selector = Selector.open(); + this.tabbedPane = tabbedPane; + protocol = ProtocolFactory.newInstance(); + mustSayGoodbye = false; + mustAnswer = false; + } + + /** + * The Event Dispatching Thread communicates with this thread through a concurrent queue. + * This is the method to add requests to the queue. + * @param r the request for this thread + */ + public void addRequest(Request r){ + requests.add(r); + selector.wakeup(); + } + + public Answer getAnswer(){ + try { + return answers.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e);// must never happen + } + } + + @Override + public void run(){ + while(!mustSayGoodbye){ + try { + selector.select(); + } catch (IOException e) { + revertAllDiagrams(); + } + + if(mustSayGoodbye) + break; + + /* handle the requests for the server from the local users */ + handleRequests(); + + for (Iterator<SelectionKey> itr = selector.selectedKeys().iterator(); itr.hasNext();){ + SelectionKey key = itr.next(); + itr.remove(); + + if(!key.isValid()) + continue; + + if(key.isReadable()){ + SocketChannel channel = (SocketChannel)key.channel(); + Message msg = null; + try { + msg = protocol.receiveMessage(channel); + } catch (IOException e) { + revertDiagram(channel); + /* signal the event dispatching thread, otherwise blocked */ + if(mustAnswer){ + try { + answers.put(new ErrorAnswer()); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + } + continue; + } + //System.out.println("ClientConnaectionManager: read message " + msg.getName()); + /* retrieve the diagram */ + @SuppressWarnings("unused") + String diagramName = msg.getDiagram(); + final Diagram diagram = channels.get(channel); + node = null; + edge = null; + if(msg instanceof Command){ + final Command cmd = (Command)msg; + /* log the command through the interaction log, if any */ + Command.log(cmd, "remote command received"); + switch(cmd.getName()){ + case INSERT_NODE : + double dx = (Double)cmd.getArgAt(1); + double dy = (Double)cmd.getArgAt(2); + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((String)cmd.getArgAt(0), diagram.getNodePrototypes()); + node = (Node)node.clone(); + /* Place the top left corner of the bounds at the origin. It might be different from * + * the origin, as it depends on how the clonation is implemented internally. These calls * + * to translate are not notified to any listener as the node is not n the model yet */ + Rectangle2D bounds = node.getBounds(); + node.translate(new Point2D.Double(),-bounds.getX(),-bounds.getY()); + /* perform the actual translation from the origin */ + node.translate(new Point2D.Double(), dx, dy); + } + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), node)); + break; + case INSERT_EDGE : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((String)cmd.getArgAt(0), diagram.getEdgePrototypes()); + edge = (Edge)edge.clone(); + List<DiagramNode> nodesToConnect = new ArrayList<DiagramNode>(cmd.getArgNum()-1); + for(int i=1;i<cmd.getArgNum();i++) + nodesToConnect.add(Finder.findNode((Long)cmd.getArgAt(i), diagram.getCollectionModel().getNodes())); + try { + edge.connect(nodesToConnect); + } catch (ConnectNodesException e) { + throw new RuntimeException();//this must never happen as the check is done by the client before issuing the command + } + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), edge)); + } + break; + case REMOVE_NODE : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(),node)); + break; + case REMOVE_EDGE : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0), diagram.getCollectionModel().getEdges()); + } + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(),edge)); + break; + case SET_EDGE_NAME : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0), diagram.getCollectionModel().getEdges()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetName(edge, (String)cmd.getArgAt(1))); + break; + case SET_NODE_NAME : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetName(node, (String)cmd.getArgAt(1))); + break; + case SET_PROPERTY : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetProperty( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + (String)cmd.getArgAt(3) + )); + break; + case SET_PROPERTIES : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + NodeProperties properties = node.getPropertiesCopy(); + properties.valueOf((String)cmd.getArgAt(1)); + SwingUtilities.invokeLater(new CommandExecutor.SetProperties( + node, + properties + )); + break; + case CLEAR_PROPERTIES : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node)); + break; + case SET_NOTES : + int[] path = new int[cmd.getArgNum()-1]; + for(int i = 0; i< cmd.getArgNum()-1;i++){ + path[i] = (Integer)cmd.getArgAt(i); + } + final String notes = (String)cmd.getArgAt(cmd.getArgNum()-1); + synchronized(diagram.getCollectionModel().getMonitor()){ + treeNode = Finder.findTreeNode(path, (DiagramModelTreeNode)diagram.getTreeModel().getRoot()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetNotes(diagram.getTreeModel(),treeNode, notes)); + break; + case ADD_PROPERTY : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.AddProperty( + node, + (String)cmd.getArgAt(1), + (String)cmd.getArgAt(2) + )); + break; + case REMOVE_PROPERTY : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2))); + break; + case SET_MODIFIERS : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + indexes = new HashSet<Integer>(cmd.getArgNum()-3); + for(int i=3;i<cmd.getArgNum();i++){ + indexes.add((Integer)cmd.getArgAt(i)); + } + } + SwingUtilities.invokeLater(new CommandExecutor.SetModifiers( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + indexes + )); + break; + case SET_ENDLABEL : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel(edge, node,(String)cmd.getArgAt(2))); + break; + case SET_ENDDESCRIPTION : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription( + edge, + node, + (Integer)cmd.getArgAt(2) + )); + break; + case TRANSLATE_NODE : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.Translate( + node, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + (Double)cmd.getArgAt(3), + (Double)cmd.getArgAt(4) + )); + break; + case TRANSLATE_EDGE : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + } + SwingUtilities.invokeLater(new CommandExecutor.Translate( + edge, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + (Double)cmd.getArgAt(3), + (Double)cmd.getArgAt(4) + )); + break; + case BEND : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + } + Point2D bendStart = null; + if(cmd.getArgNum() == 5){ + bendStart = new Point2D.Double((Double)cmd.getArgAt(3),(Double)cmd.getArgAt(4)); + } + SwingUtilities.invokeLater(new CommandExecutor.Bend( + edge, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + bendStart + )); + break; + case STOP_EDGE_MOVE : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + } + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge)); + break; + case STOP_NODE_MOVE : + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(node)); + break; + } + }else if(msg instanceof Reply){ + Reply reply = (Reply)msg; + /* log the reply through the interaction logger, if any */ + Reply.log(reply); + sendCmdRequest = null; + for(SendCmdRequest scr : pendingCommands){ + if(scr.matches(channel, reply.getDiagram())){ + sendCmdRequest = scr; + break; + } + } + assert(sendCmdRequest != null); + pendingCommands.remove(sendCmdRequest); + switch(reply.getName()){ + case INSERT_NODE_R : + case INSERT_EDGE_R : + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), sendCmdRequest.element)); + break; + case REMOVE_EDGE_R : + case REMOVE_NODE_R : + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(), sendCmdRequest.element)); + break; + case SET_EDGE_NAME_R : + case SET_NODE_NAME_R : + SwingUtilities.invokeLater(new CommandExecutor.SetName(sendCmdRequest.element, (String)sendCmdRequest.cmd.getArgAt(1))); + break; + case SET_PROPERTY_R : + SwingUtilities.invokeLater(new CommandExecutor.SetProperty( + (Node)sendCmdRequest.element, + (String)sendCmdRequest.cmd.getArgAt(1), + (Integer)sendCmdRequest.cmd.getArgAt(2), + (String)sendCmdRequest.cmd.getArgAt(3) + )); + break; + case SET_PROPERTIES_R : + Node n = (Node)sendCmdRequest.element; + NodeProperties properties = n.getPropertiesCopy(); + properties.valueOf((String)sendCmdRequest.cmd.getArgAt(1)); + SwingUtilities.invokeLater(new CommandExecutor.SetProperties( + n, + properties + )); + break; + case CLEAR_PROPERTIES_R : + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties((Node)sendCmdRequest.element)); + break; + case SET_NOTES_R : + SwingUtilities.invokeLater(new CommandExecutor.SetNotes( + diagram.getTreeModel(), + ((SendTreeCmdRequest)sendCmdRequest).treeNode, + (String)sendCmdRequest.cmd.getArgAt(sendCmdRequest.cmd.getArgNum()-1) + )); + break; + case ADD_PROPERTY_R : + SwingUtilities.invokeLater(new CommandExecutor.AddProperty( + (Node)sendCmdRequest.element, + (String)sendCmdRequest.cmd.getArgAt(1), + (String)sendCmdRequest.cmd.getArgAt(2) + )); + break; + case REMOVE_PROPERTY_R : + SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( + (Node)sendCmdRequest.element, + (String)sendCmdRequest.cmd.getArgAt(1), + (Integer)sendCmdRequest.cmd.getArgAt(2) + )); + break; + case SET_MODIFIERS_R : + indexes = new HashSet<Integer>(sendCmdRequest.cmd.getArgNum()-3); + for(int i=3;i<sendCmdRequest.cmd.getArgNum();i++){ + indexes.add((Integer)sendCmdRequest.cmd.getArgAt(i)); + } + SwingUtilities.invokeLater(new CommandExecutor.SetModifiers( + (Node)sendCmdRequest.element, + (String)sendCmdRequest.cmd.getArgAt(1), + (Integer)sendCmdRequest.cmd.getArgAt(2), + indexes + )); + break; + case SET_ENDLABEL_R : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)sendCmdRequest.cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel( + (Edge)sendCmdRequest.element, + node, + (String)sendCmdRequest.cmd.getArgAt(2) + )); + break; + case SET_ENDDESCRIPTION_R : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)sendCmdRequest.cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription( + (Edge)sendCmdRequest.element, + node, + /* if the endDescription ain't included then we have to set it to null ( = NONE )*/ + sendCmdRequest.cmd.getArgNum() == 3 ? (Integer)sendCmdRequest.cmd.getArgAt(2): -1 + )); + break; + case TRANSLATE_NODE_R : + node = (Node)sendCmdRequest.element; + SwingUtilities.invokeLater(new CommandExecutor.Translate( + node, + new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), + (Double)sendCmdRequest.cmd.getArgAt(3), + (Double)sendCmdRequest.cmd.getArgAt(4) + )); + break; + case TRANSLATE_EDGE_R : + edge = (Edge)sendCmdRequest.element; + SwingUtilities.invokeLater(new CommandExecutor.Translate( + edge, + new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), + (Double)sendCmdRequest.cmd.getArgAt(3), + (Double)sendCmdRequest.cmd.getArgAt(4) + )); + break; + case BEND_R : + edge = (Edge)sendCmdRequest.element; + Point2D bendStart = null; + if(sendCmdRequest.cmd.getArgNum() == 5){ + bendStart = new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(3),(Double)sendCmdRequest.cmd.getArgAt(4)); + } + SwingUtilities.invokeLater(new CommandExecutor.Bend( + edge, + new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), + bendStart + )); + break; + case STOP_EDGE_MOVE_R : + edge = (Edge)sendCmdRequest.element; + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge)); + break; + case STOP_NODE_MOVE_R : + node = (Node)sendCmdRequest.element; + SwingUtilities.invokeLater(new CommandExecutor.StopMove(node)); + break; + case ERROR_R : + SwingUtilities.invokeLater(new CommandExecutor.ShowErrorMessageDialog(tabbedPane, "Error for command on "+ sendCmdRequest.element.getName()+ ". " +reply.getMessage())); + InteractionLog.log("SERVER", "error:reply from server", DiagramElement.toLogString(sendCmdRequest.element) + " " +reply.getMessage()); + break; + default : throw new RuntimeException("Reply message not recognized: "+reply.getName()); + } + }else{ // lock message from the server + try { + answers.put(new LockAnswer((LockMessage)msg)); + mustAnswer = false; + } catch (InterruptedException e) { + throw new RuntimeException(e); // must never happen + } + } + } + } + } + /* this part is never reached out unless the thread is shut down */ + for(SocketChannel channel : channels.keySet()){ + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + + public void shutdown(){ + mustSayGoodbye = true; + selector.wakeup(); + } + + private void handleRequests() { + while(!requests.isEmpty()){ + Request request = requests.poll(); + if(request instanceof AddDiagramRequest){ + AddDiagramRequest adr = (AddDiagramRequest)request; + try { + adr.channel.configureBlocking(false); + adr.channel.register(selector, SelectionKey.OP_READ); + channels.put(adr.channel, adr.diagram); + } catch (IOException ioe) { + /* something went wrong, turn the diagram back into a local one */ + /* put the entry in channels just for a moment as it will be used in revertDiagram */ + channels.put(adr.channel, adr.diagram); + revertDiagram(adr.channel); + } + }else if(request instanceof RmDiagramRequest){ + RmDiagramRequest rdr = (RmDiagramRequest)request; + Set<Map.Entry<SocketChannel, Diagram>> entryset = channels.entrySet(); + for(Map.Entry<SocketChannel, Diagram> entry : entryset){ + if(entry.getValue().getName().equals(rdr.diagramName)){ + channels.remove(entry.getKey()); + try{entry.getKey().close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + }else if(request instanceof SendCmdRequest||request instanceof SendTreeCmdRequest){ + SendCmdRequest scr = (SendCmdRequest)request; + //System.out.println("ClientConnectionManager:handling request "+scr.cmd.getName()); + if(!channels.containsKey(scr.channel)) + continue; // commands issued after reverting a diagram are dropped + pendingCommands.add(scr); + try{ + protocol.send(scr.channel, scr.cmd); + }catch(IOException e){ + /* the pending commands is normally removed upon reply receive */ + pendingCommands.remove(scr); + revertDiagram(scr.channel); + } + }else if(request instanceof SendLockRequest){ + SendLockRequest slr = (SendLockRequest)request; + try { + protocol.send(slr.channel,slr.lock); + mustAnswer = true; + } catch (IOException e) { + revertDiagram(slr.channel); + try { + /* this is to unblock the event dispatching thread, which will then * + * process the code queued for execution in reverDiagram */ + answers.put(new ErrorAnswer()); + } catch (InterruptedException e1) { + throw new RuntimeException(e1); //must never happen + } + } + } + } + } + + private void revertDiagram(SocketChannel c){ + /* from now on all the commands using this channel will be dropped */ + final Diagram diagram = channels.remove(c); + if(diagram == null) + return; + try{c.close();}catch(IOException ioe){ioe.printStackTrace();} + SwingUtilities.invokeLater(new Runnable(){ + @Override + public void run() { + for(int i=0; i< tabbedPane.getTabCount();i++){ + DiagramPanel dPanel = (DiagramPanel)tabbedPane.getComponentAt(i); + if(dPanel.getDiagram() instanceof NetDiagram){ + NetDiagram netDiagram = (NetDiagram)dPanel.getDiagram(); + if( netDiagram.getDelegate().equals(diagram)){ + /* set the old (unwrapped) diagram as the current one */ + dPanel.setDiagram(diagram); + break; + } + } + } + SpeechOptionPane.showMessageDialog(tabbedPane, MessageFormat.format( + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.connection"), + diagram.getName()) + ); + } + }); + + } + + private void revertAllDiagrams(){ + for(SocketChannel c : channels.keySet()) + try{c.close();}catch(IOException ioe){ioe.printStackTrace();} + channels.clear(); + + SwingUtilities.invokeLater(new Runnable(){ + @Override + public void run() { + for(int i=0; i< tabbedPane.getTabCount();i++){ + DiagramPanel dPanel = (DiagramPanel)tabbedPane.getComponentAt(i); + if(dPanel.getDiagram() instanceof NetDiagram){ + NetDiagram netDiagram = (NetDiagram)dPanel.getDiagram(); + /* set the old (unwrapped) diagram as the current one */ + dPanel.setDiagram(netDiagram.getDelegate()); + } + } + SpeechOptionPane.showMessageDialog( + tabbedPane, + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.connections")); + } + }); + } + + public interface Request {}; + + public interface DiagramRequest extends Request {}; + + public static class AddDiagramRequest implements DiagramRequest { + public AddDiagramRequest(SocketChannel channel, Diagram diagram){ + this.channel = channel; this.diagram = diagram; + } + public SocketChannel channel; + public Diagram diagram; + } + + public static class RmDiagramRequest implements DiagramRequest { + public RmDiagramRequest(String diagramName){ + this.diagramName = diagramName; + } + public String diagramName; + } + + public static class SendCmdRequest implements Request { + public SendCmdRequest(Command cmd, SocketChannel channel, DiagramElement element ){ + this.cmd = cmd; this.element = element;this.channel = channel; + } + + public boolean matches(SocketChannel c,String diagramName){ + return(diagramName.equals(cmd.getDiagram())&&c.socket().getInetAddress().equals(channel.socket().getInetAddress())); + } + public DiagramElement element; + public SocketChannel channel; + public Command cmd; + } + + public static class SendTreeCmdRequest extends SendCmdRequest{ + public SendTreeCmdRequest( Command cmd,SocketChannel channel,DiagramModelTreeNode treeNode) { + super(cmd,channel,null); + this.treeNode = treeNode; + } + public DiagramModelTreeNode treeNode; + public SocketChannel channel; + public Command cmd; + } + + public static class SendLockRequest implements Request { + public SendLockRequest (SocketChannel channel, LockMessage lock){ + this.channel = channel; + this.lock = lock; + } + public SocketChannel channel; + public LockMessage lock; + } + + public interface Answer {}; + public static class LockAnswer implements Answer { + public LockAnswer(LockMessage answer){ + this.message = answer; + } + public LockMessage message; + } + + public static class ErrorAnswer implements Answer{ + + } + + private Node node; + private Edge edge; + private DiagramModelTreeNode treeNode; + private Set<Integer> indexes; + private SendCmdRequest sendCmdRequest; + /* for each server hold the diagram it shares with it */ + private Map<SocketChannel, Diagram> channels; + private ConcurrentLinkedQueue<Request> requests; + private BlockingQueue<Answer> answers; + private LinkedList<SendCmdRequest> pendingCommands; + private Selector selector; + private EditorTabbedPane tabbedPane; + private Protocol protocol; + private boolean mustAnswer; + private volatile boolean mustSayGoodbye; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Command.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,144 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.utils.CharEscaper; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * A command message that triggers an update in the model of the target diagram. Possible updates + * are those listed in the {@code MessageName} enum. + * Command messages are issued by both clients and server. Clients commands are sent after a + * user action. When the server receives a command it broadcasts it to all the clients but the one + * from which the command was from. + * + */ +public class Command extends Message { + public Command(Name name, String diagram, Object[] args, long timestamp){ + super(timestamp,diagram); + this.name = name; + this.args = args; + } + + public Command(Name name, String diagram, Object[] args){ + super(diagram); + this.name = name; + this.args = args; + } + + public Command(Name name, String diagram, long timestamp){ + this(name, diagram); + } + + public Command(Name name, String diagram){ + this(name, diagram, new Object[]{}); + } + + public Name getName() { + return name; + } + + public Object getArgAt(int index) { + return args[index]; + } + + public Object[] getArgs(){ + return args; + } + + public int getArgNum(){ + return args.length; + } + + + public static void log(Command cmd, String action){ + if(cmd.getName() != Command.Name.LOCAL && cmd.getName() != Command.Name.BEND + && cmd.getName() != Command.Name.TRANSLATE_EDGE && cmd.getName() != Command.Name.TRANSLATE_NODE){ + StringBuilder builder = new StringBuilder(cmd.getName().toString()); + builder.append(' ').append(cmd.getDiagram()); + for(int i=0; i<cmd.getArgNum();i++){ + builder.append(' ').append(cmd.getArgAt(i)); + } + /* replace newlines for notes so that the log has them in one line only */ + if(cmd.getName() == Command.Name.SET_NOTES){ + InteractionLog.log("SERVER", action, CharEscaper.replaceNewline(builder.toString())); + return; + } + InteractionLog.log("SERVER", action, builder.toString()); + } + } + + public static Name valueOf(String n){ + Name name = Name.NONE; + try { + name = Name.valueOf(n); + }catch(IllegalArgumentException iae){ + name.setOrigin(n); + } + return name; + } + + private Name name; + private Object[] args; + + public static enum Name implements Message.MessageName { + NONE, + LIST, + GET, + LOCAL, + INSERT_EDGE, + INSERT_NODE, + REMOVE_NODE, + REMOVE_EDGE, + SET_NODE_NAME, + SET_EDGE_NAME, + SET_PROPERTY, + SET_PROPERTIES, + CLEAR_PROPERTIES, + SET_NOTES, + ADD_PROPERTY, + REMOVE_PROPERTY, + SET_MODIFIERS, + SET_ENDDESCRIPTION, + SET_ENDLABEL, + TRANSLATE_NODE, + TRANSLATE_EDGE, + BEND, + STOP_EDGE_MOVE, + STOP_NODE_MOVE; + + private Name(){ + origin = null; + } + + private void setOrigin(String origin){ + this.origin = origin; + } + + @Override + public String toString(){ + if(origin == null) + return super.toString(); + else return origin; + } + + private String origin; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/CommandExecutor.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,301 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.awt.Component; +import java.awt.geom.Point2D; +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.GraphElement; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.SpeechOptionPane; +/** + * This class inner classes are used to invoke commands on the Event Dispatching thread by + * the networking threads receiving replies or commands by the server. Its aim + * is to preserve reference integrity in the changeover between the two threads + * + * + */ +public abstract class CommandExecutor implements Runnable { + + public static class Insert extends CommandExecutor{ + public Insert(CollectionModel<Node,Edge> m, DiagramElement de){ + element = de; + model = m; + } + @Override + public void run() { + if(element instanceof Node) + model.insert((Node)element); + else + model.insert((Edge)element); + } + private CollectionModel<Node,Edge> model; + private DiagramElement element; + } + + public static class Remove extends CommandExecutor{ + public Remove(CollectionModel<Node,Edge> m, DiagramElement de) { + model = m; + element = de; + } + @Override + public void run() { + model.takeOut(element); + } + private CollectionModel<Node,Edge> model; + private DiagramElement element; + } + + public static class SetName extends CommandExecutor { + public SetName(DiagramElement de, String n){ + element = de; + name = n; + } + @Override + public void run(){ + element.setName(name); + } + private String name; + private DiagramElement element; + } + + + public static class SetProperty extends CommandExecutor{ + public SetProperty(Node n, String t, Integer i, String v){ + node = n; + type = t; + index = i; + value = v; + } + + @Override + public void run(){ + node.setProperty(type, index, value); + } + + private Node node; + private String type; + private Integer index; + private String value; + } + + public static class SetProperties extends CommandExecutor { + public SetProperties(Node n, NodeProperties p){ + node = n; + properties = p; + } + @Override + public void run(){ + node.setProperties(properties); + } + private Node node; + private NodeProperties properties; + } + + public static class ClearProperties extends CommandExecutor { + public ClearProperties(Node n){ + node = n; + } + @Override + public void run(){ + node.clearProperties(); + } + private Node node; + } + + public static class SetNotes extends CommandExecutor{ + public SetNotes(TreeModel<Node,Edge> m, DiagramModelTreeNode tn, String n){ + model = m; + treeNode = tn; + notes = n; + } + + @Override + public void run(){ + model.setNotes(treeNode, notes); + } + private DiagramModelTreeNode treeNode; + private String notes; + private TreeModel<Node,Edge> model; + } + + public static class AddProperty extends CommandExecutor { + public AddProperty(Node n, String t, String v){ + node = n; + type = t; + value = v; + } + @Override + public void run(){ + node.addProperty(type, value); + } + private Node node; + private String type; + private String value; + } + + public static class RemoveProperty extends CommandExecutor { + public RemoveProperty(Node n, String t, int i){ + node = n; + type = t; + index = i; + } + + @Override + public void run(){ + node.removeProperty(type, index); + } + + private Node node; + private String type; + private int index; + } + + public static class SetModifiers extends CommandExecutor { + public SetModifiers(Node n,String t,Integer v, Set<Integer> i){ + node = n; + type = t; + value = v; + indexes = i; + } + @Override + public void run(){ + node.setModifierIndexes(type, value, indexes); + } + private Node node; + private String type; + private Integer value; + private Set<Integer> indexes; + } + + public static class SetEndLabel extends CommandExecutor { + public SetEndLabel(Edge e, Node n, String l){ + edge = e; + node = n; + label = l; + } + @Override + public void run(){ + edge.setEndLabel(node, label); + } + private Node node; + private Edge edge; + private String label; + } + + public static class SetEndDescription extends CommandExecutor { + public SetEndDescription(Edge e, Node n, int i){ + edge = e; + node = n; + index = i; + } + @Override + public void run(){ + edge.setEndDescription(node, index); + } + private Node node; + private Edge edge; + private int index; + } + + public static class Translate extends CommandExecutor { + public Translate(GraphElement ge, Point2D p, Double x, Double y){ + element = ge; + point = p; + dx = x; + dy = y; + } + + @Override + public void run(){ + element.translate(point, dx, dy); + } + + private GraphElement element; + private Point2D point; + private Double dx; + private Double dy; + } + + public static class StartMove extends CommandExecutor { + public StartMove(GraphElement ge, Point2D p){ + element = ge; + point = p; + } + @Override + public void run(){ + element.startMove(point); + } + private GraphElement element; + private Point2D point; + } + + public static class Bend extends CommandExecutor { + public Bend(Edge e, Point2D p, Point2D bs){ + edge = e; + point = p; + bendStart = bs; + } + @Override + public void run(){ + if(bendStart != null) + edge.startMove(bendStart); + edge.bend(point); + } + private Edge edge; + private Point2D point; + private Point2D bendStart; + } + + public static class StopMove extends CommandExecutor { + public StopMove(GraphElement ge){ + element = ge; + } + @Override + public void run(){ + element.stopMove(); + } + private GraphElement element; + } + + public static class ShowErrorMessageDialog extends CommandExecutor { + public ShowErrorMessageDialog(Component c, String msg){ + message = msg; + parentComponent = c; + } + @Override + public void run(){ + SpeechOptionPane.showMessageDialog(parentComponent, message); + } + Component parentComponent; + String message; + } + + @Override + public abstract void run(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramAlreadySharedException.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,32 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * If the user tries to share the same diagram twice, they will get this exception thrown. + * + */ +@SuppressWarnings("serial") +public class DiagramAlreadySharedException extends DiagramShareException { + DiagramAlreadySharedException(String msg){ + super(msg); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramDownloader.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,88 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + + +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; + +import uk.ac.qmul.eecs.ccmi.gui.SpeechOptionPane; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/** + * A {@code SwingWorker} that takes on the communication with the server in the very first phase of the + * connection for a new diagram. It handles the download of the list of diagrams available for sharing on the server + * and, once the user has chosen one, it downloads the diagram into the local editor. + * Since this tasks can take a long time due to network delay and the interaction has not yet started + * a {@code SwingWorker} is used so that the user interface won't get stuck in the process, the user + * being able to cancel the job and to go back to the diagram editor. + * + */ +public class DiagramDownloader extends SpeechOptionPane.ProgressDialogWorker<String,Void> { + + public DiagramDownloader(SocketChannel channel, String target, Task task){ + this.channel = channel; + this.task = task; + if(task == Task.CONNECT_AND_DOWNLOAD_LIST) + this.address = target; + else + this.diagramName = target; + } + + @Override + protected String doInBackground() throws Exception { + if(task == Task.CONNECT_AND_DOWNLOAD_LIST){ + int port = Integer.parseInt(PreferencesService.getInstance().get("server.remote_port", Server.DEFAULT_REMOTE_PORT)); + channel.connect(new InetSocketAddress(address,port)); + } + + Protocol protocol = ProtocolFactory.newInstance(); + switch(task){ + case CONNECT_AND_DOWNLOAD_LIST : + protocol.send(channel, new Command(Command.Name.LIST,"")); + break; + case DOWNLOAD_DIAGRAM : + protocol.send(channel, new Command(Command.Name.GET ,diagramName)); + } + Reply reply = protocol.receiveReply(channel); + switch(reply.getName()){ + case ERROR_R : + throw new DiagramShareException(reply.getMessage()); + case LIST_R : + String result = new String(reply.getMessage()); + if("".equals(result)) + return null; + return result; + case GET_R : + return reply.getMessage(); + default : throw new RuntimeException(); + } + } + + public static enum Task{ + CONNECT_AND_DOWNLOAD_LIST, + DOWNLOAD_DIAGRAM; + }; + + private SocketChannel channel; + private String diagramName; + private String address; + private Task task; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramShareException.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,32 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * This exception is thrown when a problem occurred in the process of sharing a diagram + * via the server with other remote users + * + */ +@SuppressWarnings("serial") +public class DiagramShareException extends Exception { + public DiagramShareException(String arg0) { + super(arg0); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/LockMessage.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,141 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * This class represents a lock message,through which the clients get exclusivity on + * editing the elements of the diagram. The class is used by both the client, + * to request a lock, and the server, to acknowledge the success/failure of the request. + * + * The argument of the message can be either the path to a three node or the id of a diagram element. + * The former is used for lock for notes editing which can concern every tree node in the tree, + * the latter for all the other types of lock as they only concern diagram elements. + * The path to a tree node is a sequence of integers representing the index of the children + * from the root to the affected node. So for instance 4,2,3 would be the third son of the + * second son of the fourth son of the root node. + */ +public class LockMessage extends Message { + + /** + * Creates a lock message for the given diagram and with the given timestamp. + */ + public LockMessage(Name name, long timestamp, String diagram, Object[] args) { + super(timestamp, diagram); + this.args = args; + this.name = name; + } + + /** + * Creates a lock message for the given diagram and timestamp of the moment + * the message is created. + */ + public LockMessage(Name name, String diagram, Object[] args) { + super(diagram); + this.args = args; + this.name = name; + } + + public LockMessage(Name name, long timestamp, String diagram, long id) { + this(name,timestamp,diagram,new Object[]{id}); + } + + public LockMessage(Name name, String diagram, long id){ + this(name,diagram,new Object[]{id}); + } + + @Override + public Name getName() { + return name; + } + + public Object getArgAt(int index) { + return args[index]; + } + + public Object[] getArgs(){ + return args; + } + + public int getArgNum(){ + return args.length; + } + + public static LockMessage.Name valueOf(String n){ + Name name = Name.NONE_L; + try { + name = Name.valueOf(n); + }catch(IllegalArgumentException iae){ + name.setOrigin(n); + } + return name; + } + + /** used to distinguish between different kinds of messages. */ + public static final String LOCK_NAME_POSTFIX = "_L"; + public static final String GET_LOCK_PREFIX = "GET_"; + public static final String YIELD_LOCK_PREFIX = "YIELD_"; + private Name name; + private Object[] args; + + /** + * enum containing all the possible lock messages that can be exchanged + * between server and client in either direction. + */ + public static enum Name implements Message.MessageName { + GET_DELETE_L, + GET_NAME_L, + GET_PROPERTIES_L, + GET_EDGE_END_L, + GET_MOVE_L, + GET_NOTES_L, + GET_BOOKMARK_L, + GET_MUST_EXIST_L, + + YIELD_DELETE_L, + YIELD_NAME_L, + YIELD_PROPERTIES_L, + YIELD_EDGE_END_L, + YIELD_MOVE_L, + YIELD_NOTES_L, + YIELD_BOOKMARK_L, + YIELD_MUST_EXISTS_L, + + YES_L, + NO_L, + NONE_L; + + private Name(){ + origin = null; + } + + private void setOrigin(String origin){ + this.origin = origin; + } + + @Override + public String toString(){ + if(origin == null) + return super.toString(); + else return origin; + } + + private String origin; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/LockMessageConverter.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,103 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.gui.Lock; + +/** + * A utility class providing static methods to convert a Lock into the LockMessage + * conveying it. And the other way around. + * + * + */ +public class LockMessageConverter { + /** + * Creates a lock message out of a lock passed as argument + * @param lock the lock to be converted + * @param isGet whether it's a "get-lock" or "yield-lock" message + * @return the lock message + */ + public static LockMessage.Name getLockMessageNamefromLock(Lock lock, boolean isGet){ + LockMessage.Name name = LockMessage.Name.NONE_L; + switch (lock){ + case DELETE : name = ((isGet) ? LockMessage.Name.GET_DELETE_L : LockMessage.Name.YIELD_DELETE_L); + break; + case NAME : name = (isGet) ? LockMessage.Name.GET_NAME_L : LockMessage.Name.YIELD_NAME_L ; + break; + case PROPERTIES : name = (isGet) ? LockMessage.Name.GET_PROPERTIES_L : LockMessage.Name.YIELD_PROPERTIES_L ; + break; + case EDGE_END : name = (isGet) ? LockMessage.Name.GET_EDGE_END_L : LockMessage.Name.YIELD_EDGE_END_L ; + break; + case MOVE : name = (isGet) ? LockMessage.Name.GET_MOVE_L : LockMessage.Name.YIELD_MOVE_L ; + break; + case NOTES : name = (isGet) ? LockMessage.Name.GET_NOTES_L : LockMessage.Name.YIELD_NOTES_L ; + break; + case BOOKMARK : name = (isGet) ? LockMessage.Name.GET_BOOKMARK_L : LockMessage.Name.YIELD_BOOKMARK_L ; + break; + case MUST_EXIST : name = (isGet) ? LockMessage.Name.GET_MUST_EXIST_L : LockMessage.Name.YIELD_MUST_EXISTS_L; + break; + } + return name; + } + + /** + * Returns the lock conveyed by the lock message passed as argument + * @param name the lock message name @see LockMessage.Name + * @return the conveyed lock + */ + public static Lock getLockFromMessageName(LockMessage.Name name){ + Lock lock = Lock.NONE; + switch(name){ + case GET_DELETE_L : + case YIELD_DELETE_L : + lock = Lock.DELETE; + break; + case GET_NAME_L : + case YIELD_NAME_L : + lock = Lock.NAME; + break; + case GET_PROPERTIES_L : + case YIELD_PROPERTIES_L : + lock = Lock.PROPERTIES; + break; + case GET_EDGE_END_L : + case YIELD_EDGE_END_L : + lock = Lock.EDGE_END; + break; + case GET_MOVE_L : + case YIELD_MOVE_L: + lock = Lock.MOVE; + break; + case GET_NOTES_L : + case YIELD_NOTES_L : + lock = Lock.NOTES; + break; + case GET_BOOKMARK_L : + case YIELD_BOOKMARK_L : + lock = Lock.BOOKMARK; + break; + case GET_MUST_EXIST_L : + case YIELD_MUST_EXISTS_L: + lock = Lock.MUST_EXIST; + break; + } + return lock; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Message.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,54 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * A basic implementation of a message exchanged between server and client. + * + */ +public abstract class Message { + + public Message(long timestamp, String diagram){ + this.timestamp = timestamp; + this.diagram = diagram; + } + + public Message(String diagram){ + this(System.currentTimeMillis(),diagram); + } + + public long getTimestamp() { + return timestamp; + } + + public String getDiagram(){ + return diagram; + } + + public abstract MessageName getName(); + + private long timestamp; + private String diagram; + + public static interface MessageName { + String toString(); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/NetDiagram.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,537 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.Queue; +import java.util.Set; + +import javax.swing.tree.TreeNode; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel; +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.DiagramModelUpdater; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.GraphElement; +import uk.ac.qmul.eecs.ccmi.gui.Lock; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PrototypePersistenceDelegate; +import uk.ac.qmul.eecs.ccmi.network.ClientConnectionManager.LockAnswer; +import uk.ac.qmul.eecs.ccmi.utils.ExceptionHandler; + +/** + * + * A NetDiagram is a diagram that is shared by connecting to a server on either a remote or local host. + * That means that other users from other computers can modify the diagram model through the server. + * A NetDiagram is created by wrapping a local diagram (a diagram open in the local editor) into a NetDiagram class. + * The wrapped diagram works as a delegate. What Really changes between a local diagram and a network diagram is + * that the modelUpdater will directly affect the diagram model for the former and exchange messages with the server + * for the latter. In the case of a network diagram the changes to the model are actually made by a {@link ClientConnectionManager} + * thread upon receiving a message from the server. + * + */ +public abstract class NetDiagram extends Diagram { + + private NetDiagram(Diagram delegateDiagram){ + this.delegateDiagram = delegateDiagram; + } + + public static NetDiagram wrapRemoteHost(Diagram diagram, ClientConnectionManager connectionManager,SocketChannel channel){ + return new RemoteHostDiagram(diagram,connectionManager,channel); + } + + public static NetDiagram wrapLocalHost(Diagram diagram, SocketChannel channel, Queue<DiagramElement> dElements, ExceptionHandler handler){ + return new LocalHostDiagram(diagram,channel,dElements,handler); + } + + @Override + public String getName(){ + return delegateDiagram.getName(); + } + + @Override + public String toString(){ + return getName(); + } + + @Override + public PrototypePersistenceDelegate getPrototypePersistenceDelegate(){ + return delegateDiagram.getPrototypePersistenceDelegate(); + } + + @Override + public TreeModel<Node,Edge> getTreeModel(){ + return delegateDiagram.getTreeModel(); + } + + @Override + public CollectionModel<Node,Edge> getCollectionModel(){ + return delegateDiagram.getCollectionModel(); + } + + @Override + public void setName(String name) { + delegateDiagram.setName(name); + } + + @Override + public Node[] getNodePrototypes() { + return delegateDiagram.getNodePrototypes(); + } + + @Override + public Edge[] getEdgePrototypes() { + return delegateDiagram.getEdgePrototypes(); + } + + @Override + public DiagramModelUpdater getModelUpdater(){ + return innerModelUpdater; + } + + public Diagram getDelegate(){ + return delegateDiagram; + } + + public abstract SocketChannel getSocketChannel(); + + protected abstract void send(Command cmd, DiagramElement element); + + protected abstract void send(Command cmd, DiagramModelTreeNode treeNode); + + protected abstract void send(LockMessage lockMessage); + + protected abstract boolean receiveLockAnswer(); + + private Diagram delegateDiagram; + private InnerModelUpdater innerModelUpdater = new InnerModelUpdater(); + + private class InnerModelUpdater implements DiagramModelUpdater { + + @Override + public boolean getLock(DiagramModelTreeNode treeNode, Lock lock) { + try { + sendLockMessage(treeNode,lock,true); + }catch(IllegalArgumentException iae){ + return false; + } + return receiveLockAnswer(); + } + + @Override + public void yieldLock(DiagramModelTreeNode treeNode, Lock lock) { + try { + sendLockMessage(treeNode,lock,false); + }catch(IllegalArgumentException iae) {} + } + + private void sendLockMessage(DiagramModelTreeNode treeNode, Lock lock, boolean isGettingLock){ + TreeNode[] path = treeNode.getPath(); + Object[] args = new Object[path.length-1]; + if(args.length == 0 && !treeNode.isRoot()) + throw new IllegalArgumentException("it's a node no longer connected with the tree"); + for(int i=0;i<path.length-1;i++){ + args[i] = delegateDiagram.getTreeModel().getIndexOfChild(path[i], path[i+1]); + } + send(new LockMessage( + LockMessageConverter.getLockMessageNamefromLock(lock,isGettingLock), + delegateDiagram.getName(), + args + )); + } + + @Override + public void insertInCollection(DiagramElement element) { + boolean isNode = false; + if(element instanceof Node) + isNode = true; + Command cmd = null; + if(isNode){ + Rectangle2D bounds = ((Node)element).getBounds(); + cmd = new Command( + Command.Name.INSERT_NODE, + delegateDiagram.getName(), + new Object[] {element.getType(),bounds.getX(),bounds.getY()} + ); + }else{ + Edge edge = (Edge)element; + Object args[] = new Object[1+edge.getNodesNum()]; + args[0] = edge.getType(); + /* the args of the command will be the id's of the connected edges */ + for(int i = 1; i< args.length; i++) + args[i] = edge.getNodeAt(i-1).getId(); + cmd = new Command( + Command.Name.INSERT_EDGE, + delegateDiagram.getName(), + args + ); + } + send(cmd,element); + } + + @Override + public void insertInTree(DiagramElement element) { + insertInCollection(element); + } + + @Override + public void takeOutFromCollection(DiagramElement element) { + boolean isNode = false; + if(element instanceof Node) + isNode = true; + Command cmd = new Command( + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + delegateDiagram.getName(), + new Object[] {element.getId()} + ); + send(cmd,element); + } + + @Override + public void takeOutFromTree(DiagramElement element) { + takeOutFromCollection(element); + } + + @Override + public void setName(DiagramElement element, String name) { + send(new Command( + element instanceof Node ? Command.Name.SET_NODE_NAME : Command.Name.SET_EDGE_NAME, + delegateDiagram.getName(), + new Object[] {element.getId(), name}), + element); + } + + @Override + public void setNotes(DiagramModelTreeNode treeNode, String notes) { + TreeNode[] path = treeNode.getPath(); + Object[] args = new Object[path.length]; + for(int i=0;i<path.length-1;i++){ + args[i] = delegateDiagram.getTreeModel().getIndexOfChild(path[i], path[i+1]); + } + args[args.length-1] = notes; + Command cmd = new Command(Command.Name.SET_NOTES, delegateDiagram.getName(),args); + send(cmd,treeNode); + } + + @Override + public void setProperty(Node node, String type, int index, String value) { + send(new Command(Command.Name.SET_PROPERTY, + delegateDiagram.getName(), + new Object[] {node.getId(),type,index,value}), + node + ); + } + + @Override + public void setProperties(Node node, NodeProperties properties) { + send(new Command(Command.Name.SET_PROPERTIES, + delegateDiagram.getName(), + new Object[] {node.getId(),properties.toString()}), + node + ); + + } + + @Override + public void clearProperties(Node node) { + send(new Command(Command.Name.CLEAR_PROPERTIES, + delegateDiagram.getName(), + new Object[] {node.getId()}), + node + ); + } + + @Override + public void addProperty(Node node, String type, String value) { + send(new Command(Command.Name.ADD_PROPERTY, + delegateDiagram.getName(), + new Object[] {node.getId(),type,value}), + node + ); + } + + @Override + public void removeProperty(Node node, String type, int index) { + send(new Command(Command.Name.REMOVE_PROPERTY, + delegateDiagram.getName(), + new Object[] {node.getId(),type,index}), + node + ); + } + + @Override + public void setModifiers(Node node, String type, int index, + Set<Integer> modifiers) { + Object args[] = new Object[modifiers.size()+3]; + args[0] = node.getId(); + args[1] = type; + args[2] = index; + int i = 0; + for(Integer I : modifiers){ + args[i+3] = I; + i++; + } + send(new Command(Command.Name.SET_MODIFIERS, delegateDiagram.getName(),args),node); + } + + @Override + public void setEndLabel(Edge edge, Node node, String label) { + send(new Command(Command.Name.SET_ENDLABEL, delegateDiagram.getName(), + new Object[] {edge.getId(), node.getId(), label}), + edge + ); + } + + @Override + public void setEndDescription(Edge edge, Node node, int index) { + send(new Command(Command.Name.SET_ENDDESCRIPTION, delegateDiagram.getName(), + new Object[] {edge.getId(), node.getId(), index}), + edge + ); + } + + @Override + public void translate(GraphElement ge, Point2D p, double dx, double dy) { + double px = 0; + double py = 0; + if(p != null){ + px = p.getX(); + py = p.getY(); + } + if(ge instanceof Node){ + Node n = (Node)ge; + send(new Command(Command.Name.TRANSLATE_NODE, delegateDiagram.getName(), + new Object[] {n.getId(), px, py, dx,dy} + ),n); + }else{ + Edge e = (Edge)ge; + send(new Command(Command.Name.TRANSLATE_EDGE, delegateDiagram.getName(), + new Object[] {e.getId(), px, py, dx,dy} + ),e); + } + } + + @Override + public void startMove(GraphElement ge, Point2D p) { + /* Store internally the point the motion started from and send a unique message * + * to the server when the edge is actually bended. This is because the lock will be * + * asked only when the mouse motion actually starts, whereas this call is done when * + * the edge is clicked down. So this variable is non null only when the first * + * bend-message is sent */ + edgeStartMovePoint = p; + } + + @Override + public void bend(Edge edge, Point2D p) { + /* send informations about the starting point only at the first time */ + if(edgeStartMovePoint == null) + send(new Command(Command.Name.BEND, delegateDiagram.getName(), + new Object[] {edge.getId(),p.getX(),p.getY()}), + edge); + else{ + send(new Command(Command.Name.BEND, delegateDiagram.getName(), + new Object[] {edge.getId(),p.getX(),p.getY(),edgeStartMovePoint.getX(),edgeStartMovePoint.getY()}), + edge); + edgeStartMovePoint = null; + } + } + + @Override + public void stopMove(GraphElement ge) { + if(ge instanceof Node){ + Node n = (Node)ge; + send(new Command(Command.Name.STOP_NODE_MOVE, delegateDiagram.getName(), + new Object[] {n.getId()}), + n); + }else{ + Edge e = (Edge)ge; + send(new Command(Command.Name.STOP_EDGE_MOVE, delegateDiagram.getName(), + new Object[] {e.getId()}), + e); + } + } + + private Point2D edgeStartMovePoint; + } + + private static class RemoteHostDiagram extends NetDiagram{ + /** + * This class wraps an existing diagram into a network diagram. + * The network diagrams returns a TreeModelNetWrap and a CollectionModelNetWrap + * when the relative getters are called + * @see TreeModelNetWrap, CollectionModelNetWrap + * + * @param diagram the diagram to wrap + * @param connectionManager a connected socket channel + */ + private RemoteHostDiagram(Diagram diagram, ClientConnectionManager connectionManager,SocketChannel channel){ + super(diagram); + this.channel = channel; + this.connectionManager = connectionManager; + } + + @Override + protected void send(Command cmd, DiagramElement element) { + connectionManager.addRequest(new ClientConnectionManager.SendCmdRequest(cmd, channel, element )); + } + + @Override + protected void send(Command cmd, DiagramModelTreeNode treeNode) { + connectionManager.addRequest(new ClientConnectionManager.SendTreeCmdRequest(cmd, channel, treeNode )); + } + + @Override + protected void send(LockMessage lockMessage){ + connectionManager.addRequest(new ClientConnectionManager.SendLockRequest(channel, lockMessage)); + } + + @Override + protected boolean receiveLockAnswer(){ + ClientConnectionManager.Answer answer = connectionManager.getAnswer(); + if(answer instanceof ClientConnectionManager.ErrorAnswer) + return false; + LockMessage.Name name = ((LockAnswer)answer).message.getName(); + switch(name){ + case YES_L : + return true; + case NO_L : + return false; + default : + throw new RuntimeException("message not recognized: "+name.toString()); + } + } + + @Override + public String getLabel(){ + return new StringBuilder(getName()) + .append(' ').append('@').append(' ') + .append(channel.socket().getInetAddress().getHostAddress()) + .toString(); + } + + @Override + public SocketChannel getSocketChannel(){ + return channel; + } + + @Override + public Object clone(){ + throw new UnsupportedOperationException(); + } + + private SocketChannel channel; + private ClientConnectionManager connectionManager; + } + + private static class LocalHostDiagram extends NetDiagram { + + private LocalHostDiagram(Diagram diagram, SocketChannel channel, Queue<DiagramElement> diagramElements, ExceptionHandler handler) { + super(diagram); + this.channel = channel; + this.diagramElements = diagramElements; + this.exceptionHandler = handler; + this.protocol = ProtocolFactory.newInstance(); + } + + @Override + protected void send(Command cmd, DiagramElement element){ + switch(cmd.getName()){ + case INSERT_NODE : + case INSERT_EDGE : + case REMOVE_NODE : + case REMOVE_EDGE : + diagramElements.add(element); + break; + } + try{ + protocol.send(channel, cmd); + }catch(IOException ioe){ + switch(cmd.getName()){ + case INSERT_NODE : + case INSERT_EDGE : + case REMOVE_NODE : + case REMOVE_EDGE : + diagramElements.remove(element); + break; + } + exceptionHandler.handleException(ioe); + } + } + + @Override + protected void send(LockMessage lockMessage) { + try { + protocol.send(channel, lockMessage); + } catch (IOException ioe) { + exceptionHandler.handleException(ioe); + } + } + + @Override + protected boolean receiveLockAnswer(){ + LockMessage answer; + try { + answer = protocol.receiveLockMessage(channel); + } catch (IOException ioe) { + exceptionHandler.handleException(ioe); + return false; + } + switch((LockMessage.Name)answer.getName()){ + case YES_L : + return true; + case NO_L : + return false; + default : + throw new RuntimeException("message not recognized: "+answer.getName().toString()); + } + } + + @Override + protected void send(Command cmd , DiagramModelTreeNode treeNode){ + try { + protocol.send(channel, cmd); + } catch (IOException ioe) { + exceptionHandler.handleException(ioe); + } + } + + @Override + public String getLabel(){ + return getName()+" @ localhost"; + } + + @Override + public SocketChannel getSocketChannel(){ + return channel; + } + + SocketChannel channel; + Queue<DiagramElement> diagramElements; + private Protocol protocol; + private ExceptionHandler exceptionHandler; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/OscProtocol.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,243 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.io.IOException; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +import de.sciss.net.OSCBundle; +import de.sciss.net.OSCMessage; + +/* + * An implementation of the Protocol interface which uses OSC messages + * streamed on a TCP connection. + * + */ +class OscProtocol implements Protocol { + + OscProtocol(){ + buffer = ByteBuffer.allocate(DEFAULT_CAPACITY); + codec = new CCmIOSCPacketCodec(); + } + + @Override + public void send(SocketChannel channel, Command cmd) throws IOException { + /* OSC message args = [diagram, cmd.arg1, cmd.arg2, amd.arg2, ... , cmd.argN ] */ + bundle = new CCmIOSCBundle(cmd.getTimestamp()); + Object[] args = new Object[1+cmd.getArgNum()]; + args[0] = cmd.getDiagram(); + for(int i=0; i<cmd.getArgNum();i++) + args[i+1] = cmd.getArgAt(i); + + bundle.addPacket(new OSCMessage(OSC_NAME_PREFIX+cmd.getName().toString(),args)); + try{ + writeBundle(channel); + }catch(IOException u){ + throw new IOException("Could not send data to the server:<" + System.identityHashCode(channel)+">",u); + } + } + + @Override + public void send(SocketChannel channel, Reply reply) throws IOException { + /* OSC message args = [messageLen, diagram, message ] */ + bundle = new CCmIOSCBundle(reply.getTimestamp()); + bundle.addPacket(new OSCMessage( + OSC_NAME_PREFIX+reply.getName().toString(), + new Object[] {reply.getMessageLen(), reply.getDiagram() ,reply.getMessage()} + ) + ); + try{ + writeBundle(channel); + }catch(IOException u){ + throw new IOException("Could not send data to the server",u); + } + } + + @Override + public void send(SocketChannel channel, LockMessage lockMessage) throws IOException { + /* OSC message args = [diagram, lock.arg1, lock.arg2, lock.arg2, ... , lock.argN ] */ + bundle = new CCmIOSCBundle(lockMessage.getTimestamp()); + Object[] args = new Object[1+lockMessage.getArgNum()]; + args[0] = lockMessage.getDiagram(); + for(int i=0; i<lockMessage.getArgNum();i++) + args[i+1] = lockMessage.getArgAt(i); + + bundle.addPacket(new OSCMessage( + OSC_NAME_PREFIX+lockMessage.getName().toString(), + args + ) + ); + try{ + writeBundle(channel); + }catch(IOException u){ + /* give a more user friendly message */ + throw new IOException("Could not send data to the server",u); + } + } + + @Override + public Command receiveCommand(SocketChannel channel) throws IOException { + OSCBundle bundle = readBundle(channel); + OSCMessage oscMessage = (OSCMessage)bundle.getPacket(0); + String name = oscMessage.getName(); + assert(name.startsWith(""+OSC_NAME_PREFIX)); + name = name.substring(1); // chop off the trailing '/' + int offset = CMD_DIAGRAM_INDEX+1; + Object args[] = new Object[oscMessage.getArgCount()-1]; + for(int i=0; i< args.length;i++) + args[i] = oscMessage.getArg(i+offset); + return new Command( + Command.valueOf(name), + (String)oscMessage.getArg(CMD_DIAGRAM_INDEX), + args, + bundle.getTimeTag() + ); + } + + @Override + public Reply receiveReply(SocketChannel channel) throws IOException { + OSCBundle bundle = readBundle(channel); + OSCMessage oscMessage = (OSCMessage)bundle.getPacket(0); + String name = oscMessage.getName(); + assert(name.startsWith(""+OSC_NAME_PREFIX)); + name = name.substring(1); // chop off the trailing '/' + @SuppressWarnings("unused") + Integer len = (Integer)oscMessage.getArg(REPLY_LEN_INDEX); // of no use at the moment + return new Reply( + Reply.valueOf(name), + (String)oscMessage.getArg(REPLY_DIAGRAM_INDEX), + (String)oscMessage.getArg(REPLY_MESSAGE_INDEX), + bundle.getTimeTag()); + } + + @Override + public LockMessage receiveLockMessage(SocketChannel channel) throws IOException { + OSCBundle bundle = readBundle(channel); + OSCMessage oscMessage = (OSCMessage)bundle.getPacket(0); + String name = oscMessage.getName(); + name = name.substring(1); // chop off the trailing '/' + Object args[] = new Object[oscMessage.getArgCount()-1]; + int offset = LOCK_DIAGRAM_INDEX + 1; + for(int i=0; i< args.length;i++) + args[i] = oscMessage.getArg(i+offset); + return new LockMessage( + LockMessage.valueOf(name), + bundle.getTimeTag(), + (String)oscMessage.getArg(LOCK_DIAGRAM_INDEX), + args + ); + } + + public Message receiveMessage(SocketChannel channel) throws IOException { + OSCBundle bundle = readBundle(channel); + OSCMessage oscMessage = (OSCMessage)bundle.getPacket(0); + String name = oscMessage.getName(); + assert(name.startsWith(""+OSC_NAME_PREFIX)); + name = name.substring(1); // chop off the trailing '/' + if(name.endsWith(Reply.REPLY_NAME_POSTFIX)){ // it's a reply + @SuppressWarnings("unused") + Integer len = (Integer)oscMessage.getArg(REPLY_LEN_INDEX); // of no use at the moment + Reply reply = new Reply( + Reply.valueOf(name), + (String)oscMessage.getArg(REPLY_DIAGRAM_INDEX), + (String)oscMessage.getArg(REPLY_MESSAGE_INDEX), + bundle.getTimeTag()); + return reply; + }else if (name.endsWith(LockMessage.LOCK_NAME_POSTFIX)){ + Object args[] = new Object[oscMessage.getArgCount()-1]; + int offset = LOCK_DIAGRAM_INDEX + 1; + for(int i=0; i< args.length;i++) + args[i] = oscMessage.getArg(i+offset); + return new LockMessage( + LockMessage.valueOf(name), + bundle.getTimeTag(), + (String)oscMessage.getArg(LOCK_DIAGRAM_INDEX), + args + ); + }else{ // it's a command + Object args[] = new Object[oscMessage.getArgCount()-1]; + int offset = CMD_DIAGRAM_INDEX + 1; + for(int i=0; i< args.length;i++) + args[i] = oscMessage.getArg(i+offset); + return new Command( + Command.valueOf(name), + (String)oscMessage.getArg(CMD_DIAGRAM_INDEX), + args, + bundle.getTimeTag() + ); + } + } + + private OSCBundle readBundle(SocketChannel channel) throws IOException{ + /* read the size of the OSC packet, first 4 bytes according to OSC specs */ + buffer.rewind().limit(4); + while( buffer.hasRemaining() ) + if( channel.read( buffer ) == -1 ) + throw new SocketException("Connection closed by peer"); + + buffer.rewind(); + int packetSize = buffer.getInt(); + assert(packetSize > 0 ); + ByteBuffer b = buffer; + /* if the packet is very big we must allocate an ad hoc temporary big big buffer */ + if(packetSize <= DEFAULT_CAPACITY) + b.rewind().limit(packetSize); + else + b = ByteBuffer.allocate(packetSize); + /* read the packet, it must be a bundle containing only one message */ + while( b.hasRemaining() ) + if( channel.read( b ) == -1 ) + throw new SocketException("Connection closed by peer"); + b.rewind(); + return (OSCBundle)codec.decode(b); + } + + private void writeBundle(SocketChannel channel) throws IOException{ + ByteBuffer b = buffer; + buffer.clear(); + if(bundle.getSize() + 4 > DEFAULT_CAPACITY){ + b = ByteBuffer.allocate(bundle.getSize() + 4); + } + b.position(4); + bundle.encode(codec,b); + int len = b.position() - 4; + b.putInt(0, len); + b.flip(); + channel.write(b); + } + + ByteBuffer buffer; + CCmIOSCBundle bundle; + CCmIOSCPacketCodec codec; + + private static final int DEFAULT_CAPACITY = 1024; + private static final char OSC_NAME_PREFIX = '/'; + + + private static final int REPLY_LEN_INDEX = 0; + /* position of the diagram Name in the OSC message */ + private static final int REPLY_DIAGRAM_INDEX = 1; + private static final int CMD_DIAGRAM_INDEX = 0; + private static final int LOCK_DIAGRAM_INDEX = 0; + /* ------------------------------------------------*/ + private static final int REPLY_MESSAGE_INDEX = 2; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Protocol.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,45 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.io.IOException; +import java.nio.channels.SocketChannel; + +/** + * Objects implementing the {@code Protocol} interface will take care of + * the underlying communication protocol used over TCP/IP sockets. + * + */ +public interface Protocol { + void send(SocketChannel channel, Command cmd) throws IOException; + + void send(SocketChannel channel, Reply reply) throws IOException; + + void send(SocketChannel channel, LockMessage lock) throws IOException; + + Reply receiveReply(SocketChannel channel) throws IOException; + + Command receiveCommand(SocketChannel channel) throws IOException; + + LockMessage receiveLockMessage(SocketChannel channel) throws IOException; + + Message receiveMessage(SocketChannel channel) throws IOException; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ProtocolFactory.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,30 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * The factory class to create {@code Protocol} instances. + * + */ +public abstract class ProtocolFactory { + public static Protocol newInstance(){ + return new OscProtocol(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Reply.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,135 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * A {@code Reply} message is sent from the server to a client from which it + * has just received a command. The reply acknowledges the client that the command has + * successfully been executed on the server. By broadcasting the command to the other client and by + * ending the reply to the client which issued the command itself the server keeps all the clients + * synchronized on its diagram model. + * + */ +public class Reply extends Message { + + public Reply(Name name, String diagram, String message, long timestamp){ + super(timestamp,diagram); + this.name = name; + this.message = message; + } + + public Reply(Name name, String diagram, String message){ + super(diagram); + this.name = name; + this.message = message; + } + + public Name getName() { + return name; + } + + public String getMessage() { + return message; + } + + /** + * @return the length of the String message conveyed by this Reply + */ + public int getMessageLen(){ + return message.length(); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + builder.append(timestamp).append(' ').append(name).append('\n').append(message); + return builder.toString(); + } + + public static Name valueOf(String n){ + Name name = Name.NONE_R; + try { + name = Name.valueOf(n); + }catch(IllegalArgumentException iae){ + name.setOrigin(n); + } + return name; + } + + public static void log(Reply reply){ + if(reply.getName() != Reply.Name.TRANSLATE_EDGE_R && reply.getName() != Reply.Name.TRANSLATE_NODE_R && reply.getName() != Reply.Name.BEND_R){ + StringBuilder builder = new StringBuilder(reply.getName().toString()); + builder.append(' ').append(reply.getDiagram()); + builder.append(' ').append(reply.getMessage()); + InteractionLog.log("SERVER", "reply received", builder.toString()); + } + } + + private Name name; + private String message; + private long timestamp; + public static final String REPLY_NAME_POSTFIX = "_R"; + public static enum Name implements Message.MessageName { + NONE_R, + OK_R, + ERROR_R, + LIST_R, + GET_R, + INSERT_NODE_R, + REMOVE_NODE_R, + INSERT_EDGE_R, + REMOVE_EDGE_R, + SET_NODE_NAME_R, + SET_EDGE_NAME_R, + SET_PROPERTY_R, + SET_PROPERTIES_R, + CLEAR_PROPERTIES_R, + SET_NOTES_R, + ADD_PROPERTY_R, + REMOVE_PROPERTY_R, + SET_MODIFIERS_R, + SET_ENDDESCRIPTION_R, + SET_ENDLABEL_R, + TRANSLATE_NODE_R, + TRANSLATE_EDGE_R, + BEND_R, + STOP_EDGE_MOVE_R, + STOP_NODE_MOVE_R; + + private Name(){ + origin = null; + } + + private void setOrigin(String origin){ + this.origin = origin; + } + + @Override + public String toString(){ + if(origin == null) + return super.toString(); + else return origin; + } + + private String origin; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Server.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,201 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Queue; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.JFrame; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.SpeechLogDialog; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/** + * The {@code Server} is a thread the user can start, which accept connections from other clients + * (other users machines running the CCmI editor). + * When a user shares a diagram via the server, other clients + * will be able to see and to modify it in real time. This is achieved by exchanging network + * message between the server and each client. + * On a shared diagram scenario the server + * diagram model is considered to be the "real" one. That is, the other clients will continuously + * synchronize their model with the server and even before applying a command issued by an action of + * the local user, clients need first send the command to the server (which will apply the command on its own model) + * and wait for an acknowledging reply. + * + */ +public class Server extends Thread { + + public static Server createServer(){ + if(server != null) + server.shutdown(server.resources.getString("log.restart")); + server = new Server(); + return server; + } + + private Server(){ + super("Server Thread"); + mustSayGoodbye = false; + initialized = false; + resources = ResourceBundle.getBundle(this.getClass().getName()); + } + + public void init(JFrame frame) throws IOException { + if(initialized) + return; + serverChannel = ServerSocketChannel.open(); + selector = Selector.open(); + diagrams = new HashMap<String,Diagram>(); + scManager = new ServerConnectionManager(diagrams); + String portAsString = PreferencesService.getInstance().get("server.local_port",Server.DEFAULT_LOCAL_PORT); + int port = Integer.parseInt(portAsString); + serverChannel.socket().bind(new InetSocketAddress(port)); + serverChannel.configureBlocking(false); + serverChannel.register(selector, SelectionKey.OP_ACCEPT); + logger.addHandler(SpeechLogDialog.getSpeechLogDialog(frame)); + logger.config("Server initialized, will listen on port: "+port); + initialized = true; + } + + @Override + public void run(){ + logger.config(resources.getString("log.start")); + running = true; + while(!mustSayGoodbye){ + try{ + selector.select(); + if(mustSayGoodbye) + break; + for (Iterator<SelectionKey> itr = selector.selectedKeys().iterator(); itr.hasNext();){ + SelectionKey key = itr.next(); + itr.remove(); + + if(!key.isValid()) + continue; + if(key.isAcceptable()){ + SocketChannel clientChannel = serverChannel.accept(); + clientChannel.configureBlocking(false); + clientChannel.register(selector, SelectionKey.OP_READ); + } + if(key.isReadable()){ + try{ + scManager.handleMessage((SocketChannel)key.channel()); + }catch(IOException ioe){ + /* Upon exception the channel is no longer considered reliable: it's cleaned up and closed */ + SocketChannel channel = (SocketChannel)key.channel(); + channel.close(); + scManager.removeChannel(channel); + logger.severe(ioe.getLocalizedMessage()); + } + } + } + }catch(IOException e){ + logger.severe(e.getLocalizedMessage()); + e.printStackTrace(); + break; + } + } + cleanup(); + } + + private void cleanup(){ + try { + /* close all the channels */ + serverChannel.close(); + for (Iterator<SelectionKey> itr = selector.keys().iterator(); itr.hasNext();){ + SelectionKey key = itr.next(); + ((Closeable)key.channel()).close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + running = false; + } + + public void shutdown(String msg){ + if(msg != null) + NarratorFactory.getInstance().speak(resources.getString("log.shutdown")+msg); + shutdown(); + } + + public void shutdown(){ + mustSayGoodbye = true; + selector.wakeup(); + server = null; + for(Handler h : logger.getHandlers()){ + h.close(); + logger.removeHandler(h); + } + } + + public Queue<DiagramElement> getLocalhostQueue(String diagramName){ + return scManager.getLocalhostMap().get(diagramName); + } + + /* this is called by the event dispatching thread when the user shares a diagram */ + public void share(Diagram diagram) throws ServerNotRunningException, DiagramAlreadySharedException{ + String name = diagram.getName(); + if(!running) + throw new ServerNotRunningException(resources.getString("exception_msg.not_run")); + synchronized(diagrams){ + if(diagrams.containsKey(name)){ + throw new DiagramAlreadySharedException(resources.getString("exception_msg.already_shared")); + } + diagrams.put(name,diagram); + } + scManager.getLocalhostMap().put(name, new ConcurrentLinkedQueue<DiagramElement>()); + logger.info("Diagram "+name+" shared on the server"); + } + + private static Server server; + public static final String DEFAULT_LOCAL_PORT = "7777"; + public static final String DEFAULT_REMOTE_PORT = "7777"; + static Logger logger; + private ServerSocketChannel serverChannel; + private ServerConnectionManager scManager; + private Selector selector; + private Map<String, Diagram> diagrams; + private ResourceBundle resources; + private boolean initialized; + private volatile boolean mustSayGoodbye; + private boolean running; + + static { + logger = Logger.getLogger(Server.class.getCanonicalName()); + logger.setUseParentHandlers(false); + logger.setLevel(Level.CONFIG); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Server.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,9 @@ + +log.start=Server started... +log.restart=Restarting server... +log.shutdown=Server shutdown: +exception_msg.not_run=Server not running +exception_msg.already_shared=Diagram already shared + +dialog.error.connection=A problem with the server occurred. {0} is no longer shared with other peers. +dialog.error.connections=A network problem occurred. All shared diagrams are no longer as such \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ServerConnectionManager.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,491 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.swing.SwingUtilities; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Finder; +import uk.ac.qmul.eecs.ccmi.gui.Lock; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; + + + +/* This class manages the different sessions with the clients. whereas the + * Server class just listens for connections, this class manages all what's going on about the + * diagram editing: e.g. command processing consistency check, client updates etc. + */ +class ServerConnectionManager { + ServerConnectionManager( Map<String,Diagram> diagrams){ + this.diagrams = diagrams; + diagramChannelAllocations = new HashMap<Diagram,Set<SocketChannel>>(); + localhostDiagramElementQueue = new ConcurrentHashMap<String,Queue<DiagramElement>>(); + protocol = ProtocolFactory.newInstance(); + lockManager = new ServerLockManager(); + } + + /** + * remove the channel and the locks related to it from the inner data structures + * @param channel the channel to remove + */ + public void removeChannel(SocketChannel channel){ + String diagramName = null; + for(Map.Entry<Diagram,Set<SocketChannel>> entry : diagramChannelAllocations.entrySet()){ + if(entry.getValue().remove(channel)){ + diagramName = entry.getKey().getName(); + } + } + lockManager.removeLocks(channel, diagramName); + } + + public void handleMessage(SocketChannel channel) throws IOException{ + Message message = protocol.receiveMessage(channel); + if(message instanceof Command){ + handleCommand((Command)message, channel); + }else { + handleLockMessage((LockMessage)message,channel); + } + } + + private void handleLockMessage(LockMessage lockMessage, SocketChannel channel) throws IOException{ + Lock lock = LockMessageConverter.getLockFromMessageName((LockMessage.Name)lockMessage.getName()); + String name = lockMessage.getName().toString(); + String diagramName = lockMessage.getDiagram(); + + /* spot the tree node the message refers to */ + int[] path = new int[lockMessage.getArgNum()]; + for(int i = 0; i< path.length;i++){ + path[i] = (Integer)lockMessage.getArgAt(i); + } + synchronized(diagram.getCollectionModel().getMonitor()){ + treeNode = Finder.findTreeNode(path, (DiagramModelTreeNode)diagram.getTreeModel().getRoot()); + } + /* the tree node has been deleted, lock cannot be granted */ + if(treeNode == null){ + replyLockMessage(channel,diagramName,false); + return; + } + //System.out.println("Lock message received: " + name +" diagram:"+diagramName+" treenode:"+treeNode.getName() ); + + /* check whether it's a GET or YIELD message and act accordingly */ + if(name.startsWith(LockMessage.GET_LOCK_PREFIX)){ + boolean succeeded = lockManager.requestLock(treeNode, lock, channel, diagramName); + replyLockMessage(channel,diagramName,succeeded); + }else{ // yield lock + lockManager.releaseLock(treeNode, lock, channel,diagramName); + } + } + + private void handleCommand(final Command cmd, SocketChannel channel) throws IOException{ + /* init some variables we're gonna use in (nearly) every branch of the switch */ + final String diagramName = cmd.getDiagram(); + synchronized(diagrams){ + diagram = diagrams.get(diagramName); + } + node = null; + edge = null; + boolean broadcast = true; + + /* log the command through the interaction logger, if any */ + Command.log(cmd,(channel == localChannel) ? "local command received" : "remote command received"); + //System.out.println("ServerConnectionManager: received command "+cmd.getName()); + switch(cmd.getName()){ + case LOCAL : + localChannel = channel; + Set<SocketChannel> list = new HashSet<SocketChannel>(); + list.add(localChannel); + diagramChannelAllocations.put(diagram, list); + broadcast = false; + break; + case LIST : + StringBuilder names = new StringBuilder(""); + synchronized(diagrams){ + for(String s : diagrams.keySet()){ + names.append(s).append('\n'); + } + } + protocol.send(channel, new Reply(Reply.Name.LIST_R,"",names.toString())); + broadcast = false; + break; + case GET : + try{ + if(diagram == null){ + protocol.send(channel, new Reply(Reply.Name.ERROR_R,diagramName,"Diagram "+diagramName+" does not exists")); + break; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PersistenceManager.encodeDiagramInstance(diagram, new BufferedOutputStream(out)); + try{ + protocol.send(channel, new Reply(Reply.Name.GET_R,diagramName,out.toString("UTF-8"))); + }catch(IOException ioe){ + throw ioe; + } + diagramChannelAllocations.get(diagram).add(channel); + broadcast = false; + }catch(Exception e){ + // log and discard the packet + Server.logger.severe(e.getMessage()); + } + break; + case INSERT_NODE : + if(channel == localChannel){ + /* if the command is coming from the local user then there is * + * a diagram element queued, which is the one the user wanted to insert */ + node = (Node)localhostDiagramElementQueue.get(diagramName).poll(); + }else{ + double dx = (Double)cmd.getArgAt(1); + double dy = (Double)cmd.getArgAt(2); + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((String)cmd.getArgAt(0),diagram.getNodePrototypes()); + node = (Node)node.clone(); + /* Place the top left corner of the bounds at the origin. It might be different from * + * the origin, as it depends on how the clonation is implemented internally. These calls * + * to translate are not notified to any listener as the node is not in the model yet */ + Rectangle2D bounds = node.getBounds(); + node.translate(new Point2D.Double(),-bounds.getX(),-bounds.getY()); + /* perform the actual translation from the origin */ + node.translate(new Point2D.Double(), dx, dy); + } + } + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(),node)); + /* send the reply to the client which issued the command */ + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.INSERT_NODE_R,diagramName,"Insert new Node")); + break; + case REMOVE_NODE : + if(channel == localChannel){ + /* if the command is coming from the local user then there is * + * a diagram element queued, which is the one the user wants to remove */ + node = (Node)localhostDiagramElementQueue.get(diagramName).poll(); + }else{ + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + } + /* wait for the Event Dispatching Thread to delete the edge, in order to avoid collisions * + * with other locks, e.g. locking again an edge before the EDT deletes it */ + try { + lockManager.removeLocks(node, diagramName); + SwingUtilities.invokeAndWait(new CommandExecutor.Remove(diagram.getCollectionModel(),node)); + } catch (Exception exception) { + throw new RuntimeException(exception); // must never happen + } + /* send the reply to the client which issued the command */ + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.REMOVE_NODE_R,diagramName,"Node with id "+ node.getId() +" removed")); + break; + case INSERT_EDGE : + if(channel == localChannel){ + /* if the command is coming from the local user then there is a diagram * + * element queued, which is the one the user wanted to insert */ + edge = (Edge)localhostDiagramElementQueue.get(diagramName).poll(); + }else{ + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((String)cmd.getArgAt(0),diagram.getEdgePrototypes()); + edge = (Edge)edge.clone(); + List<DiagramNode> nodesToConnect = new ArrayList<DiagramNode>(cmd.getArgNum()-1); + /* retrieve the nodes to connect by the id, conveyed in the message */ + for(int i = 1; i<cmd.getArgNum(); i++){ + Node attachedNode = Finder.findNode((Long)cmd.getArgAt(i),diagram.getCollectionModel().getNodes()); + nodesToConnect.add(attachedNode); + } + try { + edge.connect(nodesToConnect); + } catch (ConnectNodesException e) { + throw new RuntimeException();//this must never happen as the check is done by the local client before issuing the command + } + } + } + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), edge)); + /* send the reply to the client which issued the command */ + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.INSERT_EDGE_R,diagramName,"Insert new Edge")); + break; + case REMOVE_EDGE : + if(channel == localChannel){ + /* if the command is coming from the local user then there is a diagram */ + /* element queued, which is the one the user wanted to insert */ + edge = (Edge)localhostDiagramElementQueue.get(diagramName).poll(); + }else{ + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + } + } + /* wait for the Event Dispatching Thread to delete the edge, in order to avoid collisions * + * with other locks, e.g. locking again an edge before the EDT deletes it */ + try { + lockManager.removeLocks(edge, diagramName); + SwingUtilities.invokeAndWait(new CommandExecutor.Remove(diagram.getCollectionModel(), edge)); + } catch (Exception e) { + throw new RuntimeException(e); // must never happen + } + /* send the reply to the client which issued the command */ + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.REMOVE_EDGE_R,diagramName,"Edge with id "+ edge.getId() +" removed")); + break; + case SET_EDGE_NAME : + synchronized(diagram.getCollectionModel().getMonitor()){ + de = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetName(de,((String)cmd.getArgAt(1)))); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_EDGE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1))); + break; + case SET_NODE_NAME : + synchronized(diagram.getCollectionModel().getMonitor()){ + de = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetName(de,((String)cmd.getArgAt(1)))); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_NODE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1))); + break; + case SET_PROPERTY : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetProperty( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + (String)cmd.getArgAt(3) + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(2)+ " set to "+ cmd.getArgAt(3))); + break; + case SET_PROPERTIES : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + NodeProperties properties = node.getProperties(); + properties.valueOf((String)cmd.getArgAt(1)); + SwingUtilities.invokeLater(new CommandExecutor.SetProperties( + node, + properties + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_PROPERTIES_R,diagramName,"Properties for " + node.getName()+ " set to "+ cmd.getArgAt(1))); + break; + case CLEAR_PROPERTIES : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.CLEAR_PROPERTIES_R,diagramName,"Propertis of Node "+ node.getName() +" cleared")); + break; + case SET_NOTES : + int[] path = new int[cmd.getArgNum()-1]; + for(int i = 0; i< cmd.getArgNum()-1;i++){ + path[i] = (Integer)cmd.getArgAt(i); + } + final String notes = (String)cmd.getArgAt(cmd.getArgNum()-1); + synchronized(diagram.getCollectionModel().getMonitor()){ + treeNode = Finder.findTreeNode(path, (DiagramModelTreeNode)diagram.getTreeModel().getRoot()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetNotes(diagram.getTreeModel(),treeNode,notes)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_NOTES_R,diagramName,"Notes for " + treeNode.getName() + " successfully updated")); + break; + case ADD_PROPERTY : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.AddProperty( + node, + (String)cmd.getArgAt(1), + (String)cmd.getArgAt(2) + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.ADD_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " added to "+ cmd.getArgAt(1))); + break; + case REMOVE_PROPERTY : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2))); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.REMOVE_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " of type "+ cmd.getArgAt(1)+" removed")); + break; + case SET_MODIFIERS : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + indexes = new HashSet<Integer>(cmd.getArgNum()-3); + for(int i=3;i<cmd.getArgNum();i++){ + indexes.add((Integer)cmd.getArgAt(i)); + } + } + SwingUtilities.invokeLater(new CommandExecutor.SetModifiers( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + indexes)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_MODIFIERS_R,diagramName,"Modifiers for " + cmd.getArgAt(1)+ " successfully set")); + break; + case SET_ENDLABEL : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel(edge,node,(String)cmd.getArgAt(2))); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_ENDLABEL_R,diagramName,"Endlabel set to "+ cmd.getArgAt(2) +" for edge "+edge.getName())); + break; + case SET_ENDDESCRIPTION : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription( + edge, + node, + (Integer)cmd.getArgAt(2) + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_ENDDESCRIPTION_R,diagramName,"End description set to " + +(cmd.getArgNum() == 3 ? cmd.getArgAt(2) : Edge.NO_ARROW_STRING) + + " for edge "+edge.getName())); + break; + case TRANSLATE_NODE : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.Translate( + node, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + (Double)cmd.getArgAt(3), + (Double)cmd.getArgAt(4) + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.TRANSLATE_NODE_R,diagramName,"Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")")); + break; + case TRANSLATE_EDGE : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + } + SwingUtilities.invokeLater(new CommandExecutor.Translate( + edge, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + (Double)cmd.getArgAt(3), + (Double)cmd.getArgAt(4) + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.TRANSLATE_EDGE_R,diagramName,"Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")")); + break; + case BEND : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + } + Point2D bendStart = null; + if(cmd.getArgNum() == 5){ + bendStart = new Point2D.Double((Double)cmd.getArgAt(3),(Double)cmd.getArgAt(4)); + } + SwingUtilities.invokeLater(new CommandExecutor.Bend( + edge, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + bendStart + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.BEND_R,diagramName,"Bend at point: ("+(Double)cmd.getArgAt(1)+","+(Double)cmd.getArgAt(1)+")")); + break; + case STOP_EDGE_MOVE : + synchronized(diagram.getCollectionModel().getMonitor()){ + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + } + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.STOP_EDGE_MOVE_R,diagramName,"Undo straight bends")); + break; + case STOP_NODE_MOVE : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.StopMove(node)); + if(channel != localChannel){ + protocol.send(channel, new Reply(Reply.Name.STOP_NODE_MOVE_R,diagramName,"Stop node move")); + } + break; + default : throw new RuntimeException(cmd.getName().toString()+ " command not recognized"); + } + if(broadcast){ + /* broadcast the command to all the clients but the local (uses the same model) and the one which issued the command (got a reply already)*/ + for(SocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc != localChannel && !sc.equals(channel)) + protocol.send(sc, cmd); + } + } + } + + public Map<String, Queue<DiagramElement>> getLocalhostMap() { + return localhostDiagramElementQueue; + } + + private void replyLockMessage(SocketChannel channel, String diagramName,boolean yes) throws IOException{ + protocol.send(channel, new LockMessage( + yes ? LockMessage.Name.YES_L : LockMessage.Name.NO_L, + diagramName, + -1 + )); + } + + private Diagram diagram; + private Node node; + private Edge edge; + private DiagramElement de; + private DiagramModelTreeNode treeNode; + private Set<Integer> indexes; + /* the String key is the name of diagram, this collection is shared with the class Server * + * and it's used to retrieve the diagrams out of commands */ + private Map<String,Diagram> diagrams; + private SocketChannel localChannel; + /* this map contains all the channels bound to a diagram, so if a change * + * is made to a diagram all its channels are broadcasted through this map */ + private Map<Diagram,Set<SocketChannel>> diagramChannelAllocations; + /* this map is used to pass the reference to elements created by the local client * + * (we don't create a new object as well as we do for the nodes created by remote clients */ + private Map<String, Queue<DiagramElement>> localhostDiagramElementQueue; + private Protocol protocol; + private ServerLockManager lockManager; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ServerLockManager.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,370 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.nio.channels.SocketChannel; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.swing.tree.TreeNode; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.EdgeReferenceHolderMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.EdgeReferenceMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeReferenceMutableTreeNode; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Lock; +import uk.ac.qmul.eecs.ccmi.gui.Node; + +/** + * + * This class keeps track of the objects currently locked by the users. + * Locking is done by inserting a lock entry into a list of entries. All lock types have one or more dependencies. + * A dependency is a lock on the same or another tree node, which prevent the lock from being acquired. + * The dependency lock can also be of another type of the lock we're trying to acquire. + * For example if I want to change the arrow head of edge E, before inserting a arrow-head lock in the list + * I must be sure nobody else has already entered a delete-lock for E, as it means that user is about to + * delete the edge I want to change the arrow of. Likewise if I want to delete a node I must be sure + * nobody else inserted a note-lock on a edge reference laying within that node in the tree as that edge reference + * will go after the node is deleted. If a user tries to acquire a lock and one or more dependencies + * have been already inserted in the list by other users then the lock won't be granted. I no dependency + * exist in the list the lock entry is created and inserted. + * + * + * Lock dependencies (unless differently specified the dependency lock is on the same tree node): + * DELETE : DELETE,ARROW_HEAD,END_LABEL,MOVE,MUST_EXIST,NAME,NOTES, BOOKMARK,PROPERTIES, + * DELETE on two ended edges if this is a node, NOTES and BOOKMARK on those tree node + * that will be deleted as a result of this action. + * EDGE_END : EDGE_END, DELETE + * MOVE : MOVE, DELETE + * MUST_EXIST : DELETE + * NAME : NAME, DELETE + * NOTES : NOTES, DELETE, DELETE on Objects whose deletion would entail the deletion of this tree node + * PROPERTIES if this is a subtree of a property node. + * BOOKMARK : DELETE + * PROPERTIES : PROPERTIES, DELETE + * + * + */ +class ServerLockManager { + public ServerLockManager(){ + locksMap = new HashMap<String,List<LockEntry>>(); + } + + /* check if the specified lock is present in the lock list for the specified tree node */ + private boolean lockExists(DiagramModelTreeNode treeNode, Lock lock, List<LockEntry> locks, SocketChannel channel){ + for(LockEntry lockEntry : locks){ + if(lockEntry.treeNode.equals(treeNode) && lockEntry.lock == lock && !lockEntry.channel.equals(channel)) + return true; + } + return false; + } + + /* check if either specified locks is present in the lock list for the specified tree node */ + private boolean lockExists(DiagramModelTreeNode treeNode, Lock lock1, Lock lock2, List<LockEntry> locks, SocketChannel channel){ + for(LockEntry lockEntry : locks){ + if(lockEntry.treeNode.equals(treeNode) && (lockEntry.lock == lock1 || lockEntry.lock == lock2) && !lockEntry.channel.equals(channel) ) + return true; + } + return false; + } + + /* Check whether the lock can be granted as it does */ + /* not clash with other locks owned by other clients */ + private boolean checkLockDependencies(DiagramModelTreeNode treeNode, Lock lock, SocketChannel channel, List<LockEntry> locks){ + /* bookmarks are not shared, we only check them against delete-lock, as editing a */ + /* bookmark on a tree node that has just been would lead to an inconsistent state */ + if(lock != Lock.BOOKMARK) + for(LockEntry lockEntry : locks){ + /* if the two elements are different, there is no possible clash. Go ahead */ + if(!lockEntry.treeNode.equals(treeNode)) + continue; + /* locks are reentrant, that is if a client requests a lock he's already got * + * then he's automatically granted for that lock */ + if(lockEntry.channel.equals(channel)){ + if(lock.equals(lockEntry.lock)){ + return true; + } + continue; //if the clients has ad different lock then just go ahead + } + /* DELETE depends on all the other locks and all the other locks depend on it */ + /* (bear in mind that at this point of the code lockEntry.treeNode == treeNode) */ + if(lock == Lock.DELETE||lockEntry.lock == Lock.DELETE) + return false; + /* unless the lock is of type MUST_EXIST, if someone else has the desired lock, then the client */ + /* cannot get it that is, only MUST_EXIST can be shared by different clients at the same time */ + if(lock != Lock.MUST_EXIST && lock == lockEntry.lock) + return false; + } + + /* delete-locks on nodes and edges involved with treeNode will prevent from acquiring the lock as treeNode might */ + /* no longer exist after the editing. See the following comments to figure out what "involved" means in this context */ + if(lock == Lock.NOTES || lock == Lock.BOOKMARK){ + /* no diagram element along the path from root to this treeNode must be delete-locked or properties-locked */ + /* (as editing a property might mean delete some tree nodes and, maybe, this tree node) */ + for(TreeNode tn : treeNode.getPath()){ + if(tn instanceof DiagramElement) + if(lockExists((DiagramElement)tn,Lock.DELETE,Lock.PROPERTIES,locks,channel)) + return false; + } + /* if note-locking a reference tree node, the referred diagram element must not be delete-locked */ + /* as the reference will go as well after the diagram element will be deleted */ + if(treeNode instanceof NodeReferenceMutableTreeNode){ + NodeReferenceMutableTreeNode referenceNode = (NodeReferenceMutableTreeNode)treeNode; + if(lockExists(referenceNode.getNode(),Lock.DELETE,locks,channel)) + return false; + } + if(treeNode instanceof EdgeReferenceMutableTreeNode){ + EdgeReferenceMutableTreeNode referenceNode = (EdgeReferenceMutableTreeNode)treeNode; + if(lockExists(referenceNode.getEdge(),Lock.DELETE,locks,channel)) + return false; + } + /* if note locking an edge reference tree holder which has only one child, we cannot grant * + * the lock if the referred edge is delete-locked as the holder will be deleted as well after * + * the eventual deletion of the edge */ + if(treeNode instanceof EdgeReferenceHolderMutableTreeNode && treeNode.getChildCount() == 1){ + EdgeReferenceMutableTreeNode referenceNode = (EdgeReferenceMutableTreeNode)treeNode.getChildAt(0); + if(lockExists(referenceNode.getEdge(),Lock.DELETE,locks,channel)) + return false; + } + } + + if(lock == Lock.DELETE){ + /* all the descendants of the element must be non notes-locked or bookmark-locked */ + for(@SuppressWarnings("rawtypes") + Enumeration enumeration = treeNode.breadthFirstEnumeration(); enumeration.hasMoreElements();){ + if(lockExists((DiagramModelTreeNode)enumeration.nextElement(),Lock.NOTES,Lock.BOOKMARK,locks,channel)) + return false; + } + + if(treeNode instanceof Node){ + Node n = (Node)treeNode; + /* if we want to delete a Node we must get the lock on each attached * + * edge as they will be deleted as well */ + for(int i =0; i< n.getEdgesNum();i++){ + Edge e = n.getEdgeAt(i); + if(lockExists(e,Lock.DELETE,locks,channel)) + return false; + /* In order to delete-lock a Node, no referee must be bookmark/notes-locked. The referees * + * are the NodeReferenceTreeNode's pointing to this Node */ + for(int j=0;j<e.getChildCount();j++){ + NodeReferenceMutableTreeNode nodeRef = (NodeReferenceMutableTreeNode)e.getChildAt(j); + if(nodeRef.getNode().equals(n)) + if(lockExists(nodeRef,Lock.NOTES,Lock.BOOKMARK,locks,channel)) + return false; + } + } + } + + if(treeNode instanceof Edge){ + /* for each node check whether the reference to this edge is notes-locked */ + Edge e = (Edge)treeNode; + for(int i=0;i<e.getNodesNum();i++){ + Node n = e.getNodeAt(i); + for(int j=0;j<n.getChildCount();j++){ + if(n.getChildAt(j) instanceof EdgeReferenceHolderMutableTreeNode){ + EdgeReferenceHolderMutableTreeNode refHolder = (EdgeReferenceHolderMutableTreeNode)n.getChildAt(j); + /* someone else is editing notes on the reference holder and it has only one child */ + /* which means it will be deleted after the edge deletion, the lock cannot be granted then */ + if((refHolder.getChildCount() == 1) && lockExists(refHolder,Lock.NOTES,Lock.BOOKMARK,locks,channel)) + return false; + /* if a reference tree node pointing to this edge is notes-locked, the edge can't be deleted */ + for(int k=0;k<refHolder.getChildCount();k++){ + EdgeReferenceMutableTreeNode edgeRef = (EdgeReferenceMutableTreeNode)refHolder.getChildAt(k); + if(lockExists(edgeRef,Lock.NOTES,Lock.BOOKMARK,locks,channel)) + return false; + } + } + } + } + } + } + + /* all the checks have been passed, the client definitely deserves the lock now */ + return true; + } + + /** + * Request an editing lock for a tree node + * + * @param treeNode : the treeNode the caller is trying to lock + * @param lock : the type of lock requested + * @param channel : the channel works as a unique identifier for the clients + * @return true if the lock is successfully granted, or false otherwise (because of another client + * holding a lock which clashes with this request) + */ + public static ServerLockManager singletonLockManager; + public boolean requestLock(DiagramModelTreeNode treeNode, Lock lock, SocketChannel channel,String diagramName){ + List<LockEntry> locks = locksMap.get(diagramName); + if(locks == null){ + /* if no object in the diagram has ever been locked */ + /* there is no entry in the map and one must be created */ + locks = new LinkedList<LockEntry>(); + locksMap.put(diagramName,locks); + singletonLockManager = this; + } + /* deleting a node will cause all the attached two-ended edges to be deleted, * + * therefore we need to lock all those edges too, before */ + if(lock == Lock.DELETE && treeNode instanceof Node){ + Node n = (Node)treeNode; + for(int i=0; i<n.getEdgesNum();i++){ + if(n.getEdgeAt(i).getNodesNum() > 2) + continue; + boolean succeeded = requestLock(n.getEdgeAt(i),Lock.DELETE,channel,diagramName); + if(!succeeded){ + /* release the previously acquired locks and return a failure */ + for(int j=0;j<i;j++){ + if(n.getEdgeAt(j).getNodesNum() == 2) + releaseLock(n.getEdgeAt(j),Lock.DELETE,channel,diagramName); + } + return false; + } + } + } + + if(!checkLockDependencies(treeNode,lock,channel,locks)){ + if(lock == Lock.DELETE && treeNode instanceof Node){ + Node n = (Node)treeNode; + for(int j=0;j<n.getEdgesNum();j++){ + if(n.getEdgeAt(j).getNodesNum() == 2) + releaseLock(n.getEdgeAt(j),Lock.DELETE,channel,diagramName); + } + } + return false; + } + locks.add(new LockEntry(treeNode,lock,channel)); + //System.out.println(lockStatusDescription(diagramName)); + return true; + } + /** + * Release a lock previously acquired + * @param treeNode the tree node, whose lock is getting released + * @param lock the lock type + * @param channel the channel of the client releasing the lock + * @param diagramName the diagram whose tree node is affected by this call + */ + public void releaseLock(DiagramModelTreeNode treeNode, Lock lock, SocketChannel channel, String diagramName){ + List<LockEntry> locks = locksMap.get(diagramName); + Iterator<LockEntry> iterator = locks.iterator(); + while(iterator.hasNext()){ + LockEntry entry = iterator.next(); + if(entry.treeNode.equals(treeNode) && entry.lock == lock && entry.channel == channel){ + iterator.remove(); + if(lock == Lock.DELETE && treeNode instanceof Node) + continue; // we have to check for attached edges which must be unlocked too + else + break;// if ain't a delete lock, we found what looking for and we can stop + } + /* if a delete lock, we have to check for attached edges which must be unlocked too */ + if(lock == Lock.DELETE && entry.treeNode instanceof Edge && treeNode instanceof Node){ + Edge e = (Edge)entry.treeNode; + if(e.getNodesNum() == 2){ + if(e.getNodeAt(0).equals(treeNode) || e.getNodeAt(1).equals(treeNode)){ + iterator.remove(); + } + } + } + } + //System.out.println(lockStatusDescription(diagramName)); + } + + /** + * Removes all the locks related to a diagram element, to be called when a diagram element is deleted. + * If the diagram element is a Node all the locks of the two-ended edges attached to it will be + * removed as well. + * @param element the diagram element whose locks must be removed + * @param diagramName the diagram the element has to be removed from + */ + public void removeLocks(DiagramElement element, String diagramName){ + List<LockEntry> locks = locksMap.get(diagramName); + if(locks == null) + return; + Iterator<LockEntry> iterator = locks.iterator(); + boolean isNode = (element instanceof Node); + while(iterator.hasNext()){ + LockEntry entry = iterator.next(); + if(entry.treeNode.equals(element)){ + iterator.remove(); + } + /* remove the lock id it's a two ended edges locks attached to this Node */ + if(isNode && entry.treeNode instanceof Edge){ + Edge e = (Edge)entry.treeNode; + if(e.getNodesNum() > 2) + continue; + if(e.getNodeAt(0).equals(element) || e.getNodeAt(1).equals(element)){ + iterator.remove(); + } + } + } + } + + /** + * remove all the locks acquired by a client, this is normally called when a client disconnects + * @param channel the channel uniquely identifying the client + * @param diagramName the name of the diagram the channel is related to + */ + public void removeLocks(SocketChannel channel, String diagramName){ + if(!locksMap.containsKey(diagramName)) + /* this can happen if a client connects and downloads the diagram list * + * and then disconnects without opening any diagram */ + return; + Iterator<LockEntry> itr = locksMap.get(diagramName).iterator(); + while(itr.hasNext()) + if(itr.next().channel.equals(channel)) + itr.remove(); + } + + public String lockStatusDescription(String diagramName){ + StringBuilder builder = new StringBuilder(); + if(locksMap.containsKey(diagramName)){ + List<LockEntry> locks = locksMap.get(diagramName); + builder.append(diagramName).append('\n'); + for(LockEntry entry : locks){ + builder.append(entry.channel.socket().getInetAddress().getHostAddress()).append(' '). + append(entry.lock).append(' '). + append(entry.treeNode.getName()).append('\n'); + } + } + return builder.toString(); + } + + public void clearAllLocks(){ // should not be used unless in a debugging session + locksMap = new HashMap<String,List<LockEntry>>(); + } + + private Map<String,List<LockEntry>> locksMap; + + private static class LockEntry { + public LockEntry(DiagramModelTreeNode treeNode,Lock lock,SocketChannel channel) { + this.channel = channel; + this.lock = lock; + this.treeNode = treeNode; + } + + public SocketChannel channel; + public Lock lock; + public DiagramModelTreeNode treeNode; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ServerNotRunningException.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,31 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * Exception thrown when the user tries to share a diagram before starting the server + * + */ +@SuppressWarnings("serial") +public class ServerNotRunningException extends DiagramShareException { + ServerNotRunningException(String msg){ + super(msg); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/ArrowHead.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,131 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.io.IOException; + +/** + This class defines arrowheads of diverse shapes. +*/ +public enum ArrowHead { + TAIL("Tail"), + TRIANGLE("Triangle"), + BLACK_TRIANGLE("Black Triangle"), + V("V"), + HALF_V("Half V"), + DIAMOND("Diamond"), + BLACK_DIAMOND("Black Diamond"); + + private ArrowHead(String name) { + this.lowerCaseName = name; + } + + public static ArrowHead getArrowHeadFromString(String arrowHeadName) throws IOException{ + ArrowHead h; + String name = arrowHeadName.toUpperCase().replace(" ", "_"); + try { + h = ArrowHead.valueOf(name); + }catch (IllegalArgumentException e){ + throw new IOException(e); + } + return h; + } + + @Override + public String toString(){ + return lowerCaseName; + } + + /** + Draws the arrowhead. + @param g2 the graphics context + @param p a point on the axis of the arrow head + @param q the end point of the arrow head + */ + public void draw(Graphics2D g2, Point2D p, Point2D q){ + GeneralPath path = getPath(p, q); + Color oldColor = g2.getColor(); + if (this == BLACK_DIAMOND || this == BLACK_TRIANGLE) + g2.setColor(Color.BLACK); + else + g2.setColor(Color.WHITE); + g2.fill(path); + g2.setColor(oldColor); + Stroke oldStroke = g2.getStroke(); + g2.setStroke(new BasicStroke()); + g2.draw(path); + g2.setStroke(oldStroke); + } + + /** + Gets the path of the arrowhead + @param p a point on the axis of the arrow head + @param q the end point of the arrow head + @return the path + */ + public GeneralPath getPath(Point2D p, Point2D q){ + GeneralPath path = new GeneralPath(); + final double ARROW_ANGLE = Math.PI / 6; + final double ARROW_LENGTH = 10; + + double dx = q.getX() - p.getX(); + double dy = q.getY() - p.getY(); + double angle = Math.atan2(dy, dx); + double x1 = q.getX() + - ARROW_LENGTH * Math.cos(angle + ARROW_ANGLE); + double y1 = q.getY() + - ARROW_LENGTH * Math.sin(angle + ARROW_ANGLE); + double x2 = q.getX() + - ARROW_LENGTH * Math.cos(angle - ARROW_ANGLE); + double y2 = q.getY() + - ARROW_LENGTH * Math.sin(angle - ARROW_ANGLE); + + path.moveTo((float)q.getX(), (float)q.getY()); + path.lineTo((float)x1, (float)y1); + if (this == V) + { + path.moveTo((float)x2, (float)y2); + path.lineTo((float)q.getX(), (float)q.getY()); + } + else if (this == TRIANGLE || this == BLACK_TRIANGLE) + { + path.lineTo((float)x2, (float)y2); + path.closePath(); + } + else if (this == DIAMOND || this == BLACK_DIAMOND) + { + double x3 = x2 - ARROW_LENGTH * Math.cos(angle + ARROW_ANGLE); + double y3 = y2 - ARROW_LENGTH * Math.sin(angle + ARROW_ANGLE); + path.lineTo((float)x3, (float)y3); + path.lineTo((float)x2, (float)y2); + path.closePath(); + } + return path; + } + + private String lowerCaseName; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/CircleNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,138 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + * A cricle shaped diagram node. + * + */ +@SuppressWarnings("serial") +public class CircleNode extends EllipticalNode { + + public CircleNode(String typeName, NodeProperties properties) { + super(typeName, properties); + dataDisplayBounds = (Rectangle2D.Double)getMinBounds(); + cShape = new Ellipse2D.Double(); + cShape.setFrame(dataDisplayBounds); + } + + @Override + public Rectangle2D getMinBounds(){ + Rectangle2D r = super.getMinBounds(); + r.setFrame(r.getX(), r.getY(), r.getHeight(), r.getHeight()); + return (Rectangle2D)r; + } + + @Override + public Rectangle2D getBounds(){ + return cShape.getBounds2D(); + } + + @Override + public Shape getShape(){ + return cShape; + } + + @Override + public InputStream getSound(){ + return sound; + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + cShape.setFrame(cShape.getX() + dx, + cShape.getY() + dy, + cShape.getWidth(), + cShape.getHeight()); + super.translateImplementation(p,dx, dy); + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + super.reshapeInnerProperties(insidePropertyTypes); + double diffwh = dataDisplayBounds.getWidth() - dataDisplayBounds.getHeight(); + Rectangle2D.Double r = new Rectangle2D.Double(); + if(diffwh > 0){ + r.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY()-diffwh/2,dataDisplayBounds.getWidth(),dataDisplayBounds.getWidth()); + } else if(diffwh < 0){ + r.setFrame(dataDisplayBounds.getX()+diffwh/2,dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + }else{ + r.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + } + cShape.setFrame(super.anyInsideProperties() ? getOutBounds(r) : r); + } + + @Override + public void decode(Document doc, Element nodeTag) throws IOException{ + super.decode(doc, nodeTag); + double diffwh = dataDisplayBounds.getWidth() - dataDisplayBounds.getHeight(); + Rectangle2D.Double r = new Rectangle2D.Double(); + if(diffwh > 0){ + r.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY()-diffwh/2,dataDisplayBounds.getWidth(),dataDisplayBounds.getWidth()); + } else if(diffwh < 0){ + r.setFrame(dataDisplayBounds.getX()+diffwh/2,dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + }else{ + r.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + } + cShape.setFrame(super.anyInsideProperties() ? getOutBounds(r) : r); + + } + + @Override + public ShapeType getShapeType(){ + return ShapeType.Circle; + } + + @Override + public Object clone(){ + CircleNode n = (CircleNode)super.clone(); + n.cShape = (Ellipse2D.Double)cShape.clone(); + return n; + } + + private Ellipse2D.Double cShape; + private static InputStream sound; + static{ + sound = CircleNode.class.getResourceAsStream("audio/Circle.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/EdgeDrawSupport.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,163 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import javax.swing.JLabel; + +import uk.ac.qmul.eecs.ccmi.gui.GraphPanel; + + +/** + * Provides static methods to draw a {@code SimpleShapeEdge} on a {@code Graphics} + * + */ +public abstract class EdgeDrawSupport { + /** + Draws a string. + @param g2 the graphics context + @param p an endpoint of the segment along which to + draw the string + @param q the other endpoint of the segment along which to + draw the string + @param s the string to draw + @param center true if the string should be centered + along the segment + * + */ + + public static void drawString(Graphics2D g2, //FIXME ArrowHead class should be in the same package + Point2D p, Point2D q, ArrowHead arrow, String s, boolean center){ + if (s == null || s.length() == 0) return; + label.setText("<html>" + s + "</html>"); + label.setFont(g2.getFont()); + Dimension d = label.getPreferredSize(); + label.setBounds(0, 0, d.width, d.height); + + Rectangle2D b = getStringBounds(g2, p, q, arrow, s, center); + + Color oldColor = g2.getColor(); + g2.setColor(g2.getBackground()); + g2.fill(b); + g2.setColor(oldColor); + + g2.translate(b.getX(), b.getY()); + label.paint(g2); + g2.translate(-b.getX(), -b.getY()); + } + + public static void drawMarker(Graphics2D g2, Point2D p, Point2D q){ + Point2D attach = q; + if (p.getX() > q.getX()){ + drawMarker(g2, q, p); + return; + } + attach = new Point2D.Double((p.getX() + q.getX()) / 2, + (p.getY() + q.getY()) / 2); + Color oldColor = g2.getColor(); + g2.setColor(GraphPanel.GRABBER_COLOR); + g2.fill(new Rectangle2D.Double(attach.getX() - MARKER_SIZE / 2, attach.getY() - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE)); + g2.setColor(oldColor); + } + + /** + Computes the attachment point for drawing a string. + @param g2 the graphics context + @param p an endpoint of the segment along which to + draw the string + @param q the other endpoint of the segment along which to + draw the string + @param b the bounds of the string to draw + @param center true if the string should be centered + along the segment + @return the point at which to draw the string + */ + private static Point2D getAttachmentPoint(Graphics2D g2, + Point2D p, Point2D q, ArrowHead arrow, Dimension d, boolean center){ + final int GAP = 3; + double xoff = GAP; + double yoff = -GAP - d.getHeight(); + Point2D attach = q; + if (center){ + if (p.getX() > q.getX()){ + return getAttachmentPoint(g2, q, p, arrow, d, center); + } + attach = new Point2D.Double((p.getX() + q.getX()) / 2, + (p.getY() + q.getY()) / 2); + if (p.getY() < q.getY()) + yoff = - GAP - d.getHeight(); + else if (p.getY() == q.getY()) + xoff = -d.getWidth() / 2; + else + yoff = GAP; + } + else + { + if (p.getX() < q.getX()){ + xoff = -GAP - d.getWidth(); + } + if (p.getY() > q.getY()){ + yoff = GAP; + } + if (arrow != null){ + Rectangle2D arrowBounds = arrow.getPath(p, q).getBounds2D(); + if (p.getX() < q.getX()){ + xoff -= arrowBounds.getWidth(); + } + else{ + xoff += arrowBounds.getWidth(); + } + } + } + return new Point2D.Double(attach.getX() + xoff, attach.getY() + yoff); + } + + /** + Computes the extent of a string that is drawn along a line segment. + @param g2 the graphics context + @param p an endpoint of the segment along which to + draw the string + @param q the other endpoint of the segment along which to + draw the string + @param s the string to draw + @param center true if the string should be centered + along the segment + @return the rectangle enclosing the string + */ + private static Rectangle2D getStringBounds(Graphics2D g2, + Point2D p, Point2D q, ArrowHead arrow, String s, boolean center){ + if (g2 == null) return new Rectangle2D.Double(); + if (s == null || s.equals("")) return new Rectangle2D.Double(q.getX(), q.getY(), 0, 0); + label.setText("<html>" + s + "</html>"); + label.setFont(g2.getFont()); + Dimension d = label.getPreferredSize(); + Point2D a = getAttachmentPoint(g2, p, q, arrow, d, center); + return new Rectangle2D.Double(a.getX(), a.getY(), d.getWidth(), d.getHeight()); + } + + private static final int MARKER_SIZE = 7; + private static JLabel label = new JLabel(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/EllipticalNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,153 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + * An elliptical shaped diagram node. + * + */ +@SuppressWarnings("serial") +public class EllipticalNode extends SimpleShapeNode { + + public EllipticalNode(String typeName, NodeProperties properties){ + super(typeName, properties); + Rectangle2D r = getMinBounds(); + eShape = new Ellipse2D.Double(0,0,r.getWidth(),r.getHeight()); + } + + @Override + public ShapeType getShapeType() { + return ShapeType.Ellipse; + } + + public static Rectangle2D getOutBounds(Rectangle2D r){ + double h = r.getHeight()/2; + double w = r.getWidth()/2; + + double anglew = Math.atan(h/w); + double angleh = Math.atan(w/h); + + double a = w + h * Math.tan(angleh)/2; + double b = h + w * Math.tan(anglew)/2; + + return new Rectangle2D.Double( + r.getCenterX() - a, + r.getCenterY() - b, + 2*a, + 2*b + ); + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + super.reshapeInnerProperties(insidePropertyTypes); + eShape.setFrame(super.anyInsideProperties() ? getOutBounds(dataDisplayBounds) : dataDisplayBounds ); + } + + @Override + public void decode(Document doc, Element nodeTag) throws IOException{ + super.decode(doc, nodeTag); + eShape.setFrame(super.anyInsideProperties() ? getOutBounds(dataDisplayBounds) : dataDisplayBounds ); + } + + @Override + protected void translateImplementation(Point2D p,double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + eShape.setFrame(eShape.getX() + dx, + eShape.getY() + dy, + eShape.getWidth(), + eShape.getHeight()); + super.translateImplementation(p,dx, dy); + } + + @Override + public Rectangle2D getBounds() { + return eShape.getBounds2D(); + } + + @Override + public Point2D getConnectionPoint(Direction d) { + return calculateConnectionPoint(d, getBounds()); + } + + public static Point2D calculateConnectionPoint(Direction d, Rectangle2D bounds){ + double a = bounds.getWidth() / 2; + double b = bounds.getHeight() / 2; + double x = d.getX(); + double y = d.getY(); + double cx = bounds.getCenterX(); + double cy = bounds.getCenterY(); + + if (a != 0 && b != 0 && !(x == 0 && y == 0)){ + double t = Math.sqrt((x * x) / (a * a) + (y * y) / (b * b)); + return new Point2D.Double(cx + x / t, cy + y / t); + } + else{ + return new Point2D.Double(cx, cy); + } + } + + @Override + public Shape getShape() { + return eShape; + } + + @Override + public InputStream getSound(){ + return sound; + } + + @Override + public Object clone(){ + EllipticalNode n = (EllipticalNode)super.clone(); + n.eShape = (Ellipse2D.Double)eShape.clone(); + return n; + } + + private Ellipse2D.Double eShape; + private static InputStream sound; + static{ + sound = EllipticalNode.class.getResourceAsStream("audio/Ellipse.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/Model.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,405 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.ResourceBundle; + +/* + * The Wizard Model holds all the data the user enters during the process of creating a diagram template. + * Such data can be re-edited by the user during the same process or if they want to create + * a new diagram template out of an existing one. When the user has entered all the data the model is used + * to create an instance of the {@link Diagram class} which is then used as a prototype diagram. + * Diagram instances that the user will edit are created by cloning the relating prototype diagram. + * + */ +class Model { + Model(){ + diagramName = new Record(); + nodes = new ModelMap<Node>(); + edges = new ModelMap<Edge>(); + } + + @Override + public String toString(){// FIXME need to port the strings to the .properties file + StringBuilder builder = new StringBuilder(); + builder.append("Diagram Name is ").append(diagramName.value).append(".\n\n"); + builder.append(diagramName.value + " diagram has "+ (nodes.isEmpty() ? "no" : nodes.size()) + " node"+((nodes.size() == 1) ? "" : "s")); + if(!nodes.isEmpty()){ + builder.append(": "); + int i = 0; + for(Node n : nodes.values()){ + builder.append(n.type.value); + if(nodes.values().size() == 1) + break; + if(i == nodes.values().size() - 2){ + builder.append(" and "); + }else if(i < nodes.values().size()-1){ + builder.append(", "); + } + i++; + } + } + builder.append(".\n"); + for(Node n : nodes.values()){ + builder.append(n.type+" node has a ") + .append(n.shape + " shape.\n"); + builder.append(n.type+" node has "+ (n.properties.isEmpty() ? "no" : n.properties.size())+((n.properties.size() == 1) ? " property" : " properties")); + if(!n.properties.isEmpty()){ + builder.append(": "); + int i = 0; + for(Property p : n.properties.values()){ + builder.append(p.type.value); + if(n.properties.size() == 1) + break; + if(i == n.properties.size() - 2){ + builder.append(" and "); + }else if(i < n.properties.size()-1){ + builder.append(", "); + } + i++; + } + } + builder.append(".\n"); + for(Property p : n.properties.values()){ + builder.append(p.type+" property has position "+p.position); + if(p.position.value.equals(SimpleShapeNode.Position.Outside.toString())) + builder.append(" and shape "+p.shape+".\n"); + else + builder.append(".\n"); + builder.append(p.type+" property has "+(p.modifiers.isEmpty() ? "no" : p.modifiers.size())+(p.modifiers.size() == 1 ? " modifier" : " modifiers")); + if(!p.modifiers.isEmpty()){ + builder.append(": "); + int i = 0; + for(Modifier m : p.modifiers.values()){ + builder.append(m.type.value); + if(p.modifiers.size() == 1) + break; + if(i == p.modifiers.size() - 2){ + builder.append(" and "); + }else if(i < p.modifiers.size()-1){ + builder.append(", "); + } + i++; + } + } + builder.append(".\n"); + for(Modifier m : p.modifiers.values()){ + builder.append(m.type+ " modifier "); + if(m.format.values.length > 0) + builder.append("is formatted as "+m.format+".\n"); + else + builder.append("\n"); + } + } + } + builder.append('\n'); + builder.append(diagramName.value + " diagram has "+ (edges.isEmpty() ? "no" : edges.size()) + " edge"+((edges.size() == 1) ? "" : "s")); + if(!edges.isEmpty()){ + builder.append(": "); + int i = 0; + for(Edge e : edges.values()){ + builder.append(e.type.value); + if(edges.values().size() == 1) + break; + if(i == edges.values().size() - 2){ + builder.append(" and "); + }else if(i < edges.values().size()-1){ + builder.append(", "); + } + i++; + } + } + builder.append(".\n"); + for(Edge e : edges.values()){ + builder.append(e.type+ " edge has minimum nodes "+e.minNodes+" and maximum nodes "+e.maxNodes+".\n") + .append(e.type+ " edge has a "+ e.lineStyle+" line style.\n") + .append(e.type+" edge has "+ ((e.arrowHeads.values.length == 0) ? "no harrow heads.\n" : e.arrowHeads.values.length +" harrow heads: "+e.arrowHeads+".\n")); + } + builder.append('\n'); + builder.append("Press up and down arrow keys to go through the summary.\n"); + return builder.toString(); + } + + Record diagramName; + /* these are sets as when we edit an already existing node we just add */ + /* to the set the new node and it gets replaced */ + ModelMap<Node> nodes; + ModelMap<Edge> edges; + + static Element copy(Element src, Element dest){ + if(src instanceof Node){ + copy((Node)src,(Node)dest); + }else if (src instanceof Edge){ + copy((Edge)src,(Edge)dest); + }else if(src instanceof Property){ + copy((Property)src,(Property)dest); + }else{ + copy((Modifier)src,(Modifier)dest); + } + return dest; + } + + static void copy(Node src, Node dest){ + dest.id = src.id; + dest.type.value = src.type.value; + dest.shape.value = src.shape.value; + dest.properties.clear(); + dest.properties.putAll(src.properties); + } + + static void copy(Property src, Property dest){ + dest.id = src.id; + dest.type.value = src.type.value; + dest.shape.value = src.shape.value; + dest.position.value = src.position.value; + dest.modifiers.clear(); + dest.modifiers.putAll(src.modifiers); + } + + static void copy(Edge src, Edge dest){ + dest.id = src.id; + dest.type.value = src.type.value; + dest.lineStyle.value = src.lineStyle.value; + dest.maxNodes.value = src.maxNodes.value; + dest.minNodes.value = src.minNodes.value; + dest.arrowHeads.values = src.arrowHeads.values; + dest.arrowHeadsDescriptions.values = src.arrowHeadsDescriptions.values; + } + + static void copy(Modifier src, Modifier dest){ + dest.id = src.id; + dest.type.value = src.type.value; + dest.format.values = src.format.values; + dest.affix.values = src.affix.values; + } + + static class Record{ + Record(){ + value = ""; + } + Record(String value){ + this.value = value; + } + @Override + public String toString(){ + return value; + } + String value; + } + + static class StrArrayRecord{ + String [] values; + StrArrayRecord(){ + values = new String[0]; + } + + StrArrayRecord(String[] values){ + this.values = values; + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(int i=0; i<values.length; i++){ + builder.append(values[i]); + if(values.length == 1) + break; + if(i == values.length - 2) + builder.append(" and "); + else if(i < values.length -1 ) + builder.append(", "); + } + return builder.toString(); + } + } + + private static long uniqueId = 0; + static class Element { + Element(){ + type = new Record(); + id = uniqueId++; + } + + void clear () { + type.value = ""; + id = uniqueId++; + } + long id; + Record type; + } + + static class Node extends Element{ + Node(){ + shape = new Record(); + properties = new ModelMap<Property>(); + } + + @Override + void clear(){ + super.clear(); + shape.value = ""; + properties.clear(); + } + Record shape; + ModelMap<Property> properties; + } + + static class Edge extends Element { + Edge(){ + lineStyle = new Record(); + minNodes = new Record(); + maxNodes = new Record(); + arrowHeads = new StrArrayRecord(){ + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(int i=0; i<values.length; i++){ + builder.append(values[i]+" with label "+arrowHeadsDescriptions.values[i]); + if(values.length == 1) + break; + if(i == values.length - 2) + builder.append(" and "); + else if(i < values.length -1 ) + builder.append(", "); + } + return builder.toString(); + } + }; + arrowHeadsDescriptions = new StrArrayRecord(); + } + @Override + public void clear(){ + super.clear(); + lineStyle.value = ""; + minNodes.value = ""; + maxNodes.value = ""; + arrowHeads.values = new String[0]; + arrowHeadsDescriptions.values = new String[0]; + } + Record lineStyle; + Record minNodes; + Record maxNodes; + StrArrayRecord arrowHeads; + StrArrayRecord arrowHeadsDescriptions; + } + + static class Property extends Element{ + Property(){ + position = new Model.Record(); + shape = new Model.Record(); + modifiers = new ModelMap<Modifier>(); + } + Record position; + Record shape; + ModelMap<Modifier> modifiers; + + @Override + void clear(){ + super.clear(); + modifiers.clear(); + position.value = ""; + shape.value = ""; + } + } + + static class Modifier extends Element { + StrArrayRecord format; + StrArrayRecord affix; + + Modifier(){ + affix = new StrArrayRecord(); + format = new StrArrayRecord(){ + @Override + public String toString(){ + ResourceBundle resources = ResourceBundle.getBundle(SpeechWizardDialog.class.getName()); + StringBuilder builder = new StringBuilder(); + for(int i=0; i<values.length; i++){ + builder.append(values[i]); + if(values[i].equals(resources.getString("modifier.format.prefix")) && !affix.values[0].isEmpty()){ + builder.append(' ').append(affix.values[0]); + } + if(values[i].equals(resources.getString("modifier.format.suffix")) && !affix.values[1].isEmpty()){ + builder.append(' ').append(affix.values[1]); + } + + if(values.length == 1) + break; + if(i == values.length - 2) + builder.append(" and "); + else if(i < values.length -1 ) + builder.append(", "); + } + return builder.toString(); + } + }; + + /* affix is always length = 2 as it only contains prefix and suffix string */ + /* eventually it contains empty strings but its length is not variable */ + affix.values = new String[2]; + } + + @Override + void clear(){ + super.clear(); + format.values = new String[0]; + affix.values = new String[2]; + } + } +} + + +@SuppressWarnings("serial") +class ModelMap<T extends Model.Element> extends LinkedHashMap<Long,T> { + public ModelMap(){ + namesMap = new LinkedHashMap<Long,String>(); + } + + @Override + public void clear(){ + namesMap.clear(); + super.clear(); + } + + public T put(Long key, T value){ + namesMap.put(key, value.type.value); + return super.put(key, value); + } + + public void putAll(Map<? extends Long,? extends T> m){ + for(Map.Entry<? extends Long,? extends T> entry : m.entrySet()){ + namesMap.put(entry.getKey(), entry.getValue().type.value); + } + super.putAll(m); + } + + public T remove(Object key){ + namesMap.remove(key); + return super.remove(key); + } + + public Collection<String> getNames(){ + return namesMap.values(); + } + + private Map<Long,String> namesMap; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/ModifierView.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,75 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +/** + * This immutable class represents the view associated with the modifier type + * associated with it in a {@link NodeProperties} instance. + * + * */ +public class ModifierView { + + public ModifierView(boolean underline, boolean bold, boolean italic, + String prefix, String postfix) { + this.underline = underline; + this.bold = bold; + this.italic = italic; + this.prefix = prefix; + this.suffix = postfix; + } + + /* Getters */ + public boolean isUnderline() { + return underline; + } + + public boolean isBold() { + return bold; + } + + public boolean isItalic() { + return italic; + } + + public String getPrefix() { + return prefix; + } + + public String getSuffix() { + return suffix; + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder("ModifierView: "); + builder.append("bold ").append(bold).append("; "); + builder.append("italic ").append(italic).append("; "); + builder.append("underline ").append(underline).append("; "); + builder.append("prefix ").append(prefix).append("; "); + builder.append("suffix ").append(suffix).append("; "); + return builder.toString(); + } + + private boolean underline; + private boolean bold; + private boolean italic; + private String prefix; + private String suffix; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/MultiLineString.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,267 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import javax.swing.JLabel; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; + +/** + A string that can extend over multiple lines. +*/ +public class MultiLineString { + /** + Constructs an empty, centered, normal size multiline + string. + */ + public MultiLineString(){ + text = ""; + arrayText = null; + justification = CENTER; + size = NORMAL; + isSingleLine = true; + } + /** + Sets the value of the text property. + @param newValue the text of the multiline string + */ + public void setText(String newValue){ + setText(newValue,null); + } + + public void setText(String newValue, ModifierView[] modifierViews) { + isSingleLine = true; + text = newValue; + this.modifierViews = modifierViews; + format(); + } + + public void setText(String[] newValue, Modifiers modifiers){ + isSingleLine = false; + this.modifiers = modifiers; + arrayText = newValue; + format(); + } + + public void setBold(boolean bold){ + isBold = bold; + format(); + } + /** + Gets the value of the text property. + @return the text of the multiline string + */ + public String getText() { + if(isSingleLine) + return text; + else{ + StringBuilder builder = new StringBuilder(""); + for(int i=0; i<arrayText.length; i++){ + builder.append(arrayText[i]); + builder.append('\n'); + } + return builder.toString(); + } + } + /** + Sets the value of the justification property. + @param newValue the justification, one of LEFT, CENTER, + RIGHT + */ + public void setJustification(int newValue) { justification = newValue; format(); } + /** + Gets the value of the justification property. + @return the justification, one of LEFT, CENTER, + RIGHT + */ + public int getJustification() { return justification; } + /** + Sets the value of the size property. + @param newValue the size, one of SMALL, NORMAL, LARGE + */ + public void setSize(int newValue) { size = newValue; format(); } + /** + Gets the value of the size property. + @return the size, one of SMALL, NORMAL, LARGE + */ + public int getSize() { return size; } + + @Override + public String toString(){ + if(isSingleLine) + return text.replace('\n', '|'); + else + return getText().replace('\n', '|'); + } + + private void format(){ + StringBuffer prefix = new StringBuffer(); + StringBuffer suffix = new StringBuffer(); + StringBuffer htmlText = new StringBuffer(); + prefix.append(" "); + suffix.insert(0, " "); + if (size == LARGE){ + prefix.append("<font size=\"+1\">"); + suffix.insert(0, "</font>"); + } + if (size == SMALL){ + prefix.append("<font size=\"-1\">"); + suffix.insert(0, "</font>"); + } + + htmlText.append("<html>"); + if(isSingleLine){ + if(isBold) + prefix.append("<b>"); + htmlText.append(prefix); + + String formattedText = text; + if(modifierViews != null){ + for(ModifierView view : modifierViews){ + formattedText = formatFromView(formattedText,view); + } + } + htmlText.append(formattedText); + + if(isBold) + suffix.insert(0, "</b>"); + htmlText.append(suffix); + }else{ // multi line + boolean first = true; + for(int i=0; i<arrayText.length; i++){ + if (first) + first = false; + else + htmlText.append("<br>"); + htmlText.append(prefix); + String textLine = arrayText[i]; + Set<Integer> indexes = modifiers.getIndexes(i); + for(Integer I : indexes){ + ModifierView view = (ModifierView)modifiers.getView(modifiers.getTypes().get(I)); + textLine = formatFromView(textLine, view); + } + htmlText.append(textLine); + htmlText.append(suffix); + } + } + htmlText.append("</html>"); + + // replace any < that are not followed by {u, i, b, tt, font, br} with < + List<String> dontReplace = Arrays.asList(new String[] { "u", "i", "b", "tt", "font", "br" }); + + int ltpos = 0; + while (ltpos != -1){ + ltpos = htmlText.indexOf("<", ltpos + 1); + if (ltpos != -1 && !(ltpos + 1 < htmlText.length() && htmlText.charAt(ltpos + 1) == '/')){ + int end = ltpos + 1; + while (end < htmlText.length() && Character.isLetter(htmlText.charAt(end))) end++; + if (!dontReplace.contains(htmlText.substring(ltpos + 1, end))) + htmlText.replace(ltpos, ltpos+1, "<"); + } + } + + label.setText(htmlText.toString()); + if (justification == LEFT) label.setHorizontalAlignment(JLabel.LEFT); + else if (justification == CENTER) label.setHorizontalAlignment(JLabel.CENTER); + else if (justification == RIGHT) label.setHorizontalAlignment(JLabel.RIGHT); + } + + private String formatFromView(String text, ModifierView view){ + if(view == null) + return text; + + StringBuilder prefix = new StringBuilder(""); + StringBuilder suffix = new StringBuilder(""); + + prefix.append(view.getPrefix()); + suffix.append(view.getSuffix()); + + if(view.isBold()){ + prefix.insert(0,"<b>"); + suffix.append("</b>"); + } + + if(view.isItalic()){ + prefix.insert(0,"<i>"); + suffix.append("</i>"); + } + + if(view.isUnderline()){ + prefix.insert(0,"<u>"); + suffix.append("</u>"); + } + return prefix.append(text).append(suffix).toString(); + } + + /** + Gets the bounding rectangle for this multiline string. + @param g2 the graphics context + @return the bounding rectangle (with top left corner (0,0)) + */ + public Rectangle2D getBounds(){ + if(isSingleLine){ + if (text.length() == 0) + return new Rectangle2D.Double(); + }else { + if(arrayText.length == 0) + return new Rectangle2D.Double(); + } + Dimension dim = label.getPreferredSize(); + return new Rectangle2D.Double(0, 0, dim.getWidth(), dim.getHeight()); + } + + /** + Draws this multiline string inside a given rectangle + @param g2 the graphics context + @param r the rectangle into which to place this multiline string + */ + public void draw(Graphics2D g2, Rectangle2D r){ + label.setFont(g2.getFont()); + label.setBounds(0, 0, (int) r.getWidth(), (int) r.getHeight()); + g2.translate(r.getX(), r.getY()); + label.paint(g2); + g2.translate(-r.getX(), -r.getY()); + } + + public static final int LEFT = 0; + public static final int CENTER = 1; + public static final int RIGHT = 2; + public static final int LARGE = 3; + public static final int NORMAL = 4; + public static final int SMALL = 5; + + private String text; + private String[] arrayText; + private Modifiers modifiers; + private ModifierView[] modifierViews; + private int justification; + private int size; + private boolean isBold; + private transient JLabel label = new JLabel(); + private boolean isSingleLine; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/PropertyView.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,48 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapeNode.Position; +import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapeNode.ShapeType; + +/** + * This immutable class represents the view associated with the property type + * associated with it in a {@link NodeProperties} instance. + * + * */ +public class PropertyView { + public PropertyView(Position position, ShapeType shapeType) { + super(); + this.position = position; + this.shapeType = shapeType; + } + + /* Getters */ + public SimpleShapeNode.Position getPosition() { + return position; + } + + public SimpleShapeNode.ShapeType getShapeType() { + return shapeType; + } + + private SimpleShapeNode.Position position; + private SimpleShapeNode.ShapeType shapeType; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/RectangularNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,143 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + A rectangular shaped diagram node. +*/ +@SuppressWarnings("serial") +public class RectangularNode extends SimpleShapeNode{ + + public RectangularNode(String nodeType, NodeProperties properties){ + super(nodeType, properties); + bounds = dataDisplayBounds; + } + + @Override + public ShapeType getShapeType() { + return ShapeType.Rectangle; + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + + bounds.setFrame(bounds.getX() + dx, + bounds.getY() + dy, + bounds.getWidth(), + bounds.getHeight()); + super.translateImplementation(p, dx, dy); + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + super.reshapeInnerProperties(insidePropertyTypes); + bounds.setFrame(dataDisplayBounds); + } + + @Override + public void decode(Document doc, Element nodeTag) throws IOException{ + super.decode(doc, nodeTag); + bounds.setFrame(dataDisplayBounds); + } + + @Override + public Rectangle2D getBounds(){ + return (Rectangle2D)bounds.clone(); + } + + @Override + public Point2D getConnectionPoint(Direction d){ + return calculateConnectionPoint(d, getBounds()); + } + + public static Point2D calculateConnectionPoint(Direction d, Rectangle2D bounds){ + double slope = bounds.getHeight() / bounds.getWidth(); + double ex = d.getX(); + double ey = d.getY(); + double x = bounds.getCenterX(); + double y = bounds.getCenterY(); + + if (ex != 0 && -slope <= ey / ex && ey / ex <= slope){ + // intersects at left or right boundary + if (ex > 0){ + x = bounds.getMaxX(); + y += (bounds.getWidth() / 2) * ey / ex; + }else{ + x = bounds.getX(); + y -= (bounds.getWidth() / 2) * ey / ex; + } + }else if (ey != 0){ + // intersects at top or bottom + if (ey > 0){ + x += (bounds.getHeight() / 2) * ex / ey; + y = bounds.getMaxY(); + }else{ + x -= (bounds.getHeight() / 2) * ex / ey; + y = bounds.getY(); + } + } + return new Point2D.Double(x, y); + } + + public Shape getShape(){ + return bounds; + } + + @Override + public InputStream getSound(){ + return sound; + } + + public Object clone(){ + RectangularNode cloned = (RectangularNode)super.clone(); + cloned.bounds = (Rectangle2D.Double)bounds.clone(); + return cloned; + } + + private Rectangle2D.Double bounds; + private static InputStream sound; + + static{ + sound = RectangularNode.class.getResourceAsStream("audio/Rectangle.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeEdge.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,321 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.GraphElement; +import uk.ac.qmul.eecs.ccmi.gui.LineStyle; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +@SuppressWarnings("serial") +public class SimpleShapeEdge extends Edge { + + public SimpleShapeEdge(String type, LineStyle style, ArrowHead[] heads, String[] availableEndDescriptions, int minAttachedNodes, int maxAttachedNodes) { + super(type,availableEndDescriptions,minAttachedNodes,maxAttachedNodes,style); + this.heads = heads; + currentHeads = new HashMap<Node,ArrowHead>(); + } + + @Override + public boolean removeNode(DiagramNode n){ + currentHeads.remove(n); + return super.removeNode(n); + } + + @Override + public void draw(Graphics2D g2) { + Stroke oldStroke = g2.getStroke(); + g2.setStroke(getStyle().getStroke()); + + /* straight line */ + if(points.isEmpty()){ + Line2D line = getSegment(getNodeAt(0),getNodeAt(1)); + g2.draw(line); + + /* draw arrow heads if any */ + ArrowHead h = currentHeads.get(getNodeAt(0)); + if( h != null && h != ArrowHead.TAIL){ + Line2D revLine = getSegment(getNodeAt(1),getNodeAt(0)); + h.draw(g2, revLine.getP1(), revLine.getP2()); + } + h = currentHeads.get(getNodeAt(1)); + if( h != null && h != ArrowHead.TAIL){ + h.draw(g2, line.getP1(), line.getP2()); + } + + /* draw labels if any */ + String label; + if((label = getEndLabel(getNodeAt(0))) != null){ + EdgeDrawSupport.drawString(g2, line.getP2(), line.getP1(), currentHeads.get(getNodeAt(0)), label, false); + } + if((label = getEndLabel(getNodeAt(1))) != null){ + EdgeDrawSupport.drawString(g2, line.getP1(), line.getP2(), currentHeads.get(getNodeAt(1)), label, false); + } + + /* draw name if any */ + if(!getName().isEmpty()){ + EdgeDrawSupport.drawString(g2, line.getP2(), line.getP1(), null, getName(), true); + } + if(!"".equals(getNotes())) + EdgeDrawSupport.drawMarker(g2,line.getP1(),line.getP2()); + }else{ + /* edge with inner points: it can be a multiended(eventually bended) edge or a straight bended edge */ + + /* arrow and labels are drawn in the same way in either case */ + for(InnerPoint p : points){ + for(GraphElement ge : p.getNeighbours()){ + g2.draw(getSegment(p,ge)); + if(ge instanceof Node){ // this is the inner point which is connected to a Node + /* draw arrow if any */ + ArrowHead h = currentHeads.get((Node)ge); + if(h != null && h != ArrowHead.TAIL){ + Line2D line = getSegment(p,ge); + h.draw(g2, line.getP1() , line.getP2()); + } + + /* draw label if any */ + String label = getEndLabel((Node)ge); + if(label != null){ + Line2D line = getSegment(p,ge); + EdgeDrawSupport.drawString(g2, line.getP1(), line.getP2(), currentHeads.get((Node)ge), label, false); + } + } + } + p.draw(g2); + } + /* name is drawn differently : + * for multiended edges name is drawn on the master inner point + * for two ends bended name is drawn in (about) the middle point of the edge + */ + + if(masterInnerPoint != null){/* multiended edge */ + Rectangle2D bounds = masterInnerPoint.getBounds(); + Point2D p = new Point2D.Double(bounds.getCenterX() - 1,bounds.getCenterY()); + Point2D q = new Point2D.Double(bounds.getCenterX() + 1,bounds.getCenterY()); + if(!getName().isEmpty()) + EdgeDrawSupport.drawString(g2, p, q, null, getName(), true); + if(!"".equals(getNotes())) + EdgeDrawSupport.drawMarker(g2,p,q); + }else{ + /* straight edge which has been bended */ + GraphElement ge1 = getNodeAt(0); + GraphElement ge2 = getNodeAt(1); + InnerPoint c1 = null; + InnerPoint c2 = null; + + for(InnerPoint innp : points){ + if(innp.getNeighbours().contains(ge1)){ + c1 = innp; + } + if(innp.getNeighbours().contains(ge2)){ + c2 = innp; + } + } + + /* draw name if any */ + if(!getName().isEmpty()){ + /* we only have two nodes but the edge has been bended */ + while((c1 != c2)&&(!c2.getNeighbours().contains(c1))){ + if(c1.getNeighbours().get(0) == ge1){ + ge1 = c1; + c1 = (InnerPoint)c1.getNeighbours().get(1); + } + else{ + ge1 = c1; + c1 = (InnerPoint)c1.getNeighbours().get(0); + } + if(c2.getNeighbours().get(0) == ge2){ + ge2 = c2; + c2 = (InnerPoint)c2.getNeighbours().get(1); + } + else{ + ge2 = c2; + c2 = (InnerPoint)c2.getNeighbours().get(0); + } + } + + Point2D p = new Point2D.Double(); + Point2D q = new Point2D.Double(); + if(c1 == c2){ + Rectangle2D bounds = c1.getBounds(); + p.setLocation( bounds.getCenterX() - 1,bounds.getCenterY()); + q.setLocation( bounds.getCenterX() + 1,bounds.getCenterY()); + }else{ + Rectangle2D bounds = c1.getBounds(); + p.setLocation( bounds.getCenterX(),bounds.getCenterY()); + bounds = c2.getBounds(); + q.setLocation(bounds.getCenterX(),bounds.getCenterY()); + + } + if(!getName().isEmpty()) + EdgeDrawSupport.drawString(g2, p, q, null, getName(), true); + if(!"".equals(getNotes())) + EdgeDrawSupport.drawMarker(g2,p,q); + } + } + } + g2.setStroke(oldStroke); + } + + public Rectangle2D getBounds() { + if(points.isEmpty()){ + return getSegment(getNodeAt(0), getNodeAt(1)).getBounds2D(); + }else{ + Rectangle2D bounds = points.get(0).getBounds(); + for(InnerPoint p : points){ + for(GraphElement ge : p.getNeighbours()) + bounds.add(getSegment(p,ge).getBounds2D()); + } + return bounds; + } + } + + public ArrowHead[] getHeads() { + return heads; + } + + @Override + public InputStream getSound(){ + switch(getStyle()){ + case Dashed : return dashedSound; + case Dotted : return dottedSound; + default : return straightSound; + } + } + + @Override + public void setEndDescription(DiagramNode diagramNode, int index){ + Node n = (Node)diagramNode; + if(index == NO_END_DESCRIPTION_INDEX){ + currentHeads.remove(n); + super.setEndDescription(n, index); + }else{ + ArrowHead h = heads[index]; + currentHeads.put(n, h); + super.setEndDescription(n, index); + } + } + + @Override + public void encode(Document doc, Element parent, List<Node> nodes){ + super.encode(doc, parent, nodes); + /* add the head attribute to the NODE tag */ + NodeList nodeTagList = parent.getElementsByTagName(PersistenceManager.NODE); + for(int i = 0 ; i< nodeTagList.getLength(); i++){ + Element nodeTag = (Element)nodeTagList.item(i); + String nodeIdAsString = nodeTag.getAttribute(PersistenceManager.ID); + long nodeId = Long.parseLong(nodeIdAsString); + Node node = null; + /* find the node with the id of the tag */ + for(Node n : nodes) + if(n.getId() == nodeId){ + node = n; + break; + } + String head = (currentHeads.get(node) == null) ? "" : currentHeads.get(node).toString(); + nodeTag.setAttribute(SimpleShapePrototypePersistenceDelegate.HEAD, head ); + } + } + + @Override + public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{ + super.decode(doc, edgeTag, nodesId); + NodeList nodeList = edgeTag.getElementsByTagName(PersistenceManager.NODE); + for(int i=0; i<nodeList.getLength(); i++){ + Element nodeTag = (Element)nodeList.item(i); + String id = nodeTag.getAttribute(PersistenceManager.ID); + if(id.isEmpty()) + throw new IOException(); + String head = nodeTag.getAttribute(SimpleShapePrototypePersistenceDelegate.HEAD); + if(!head.isEmpty()){ + ArrowHead headShape = null; + try{ + headShape = ArrowHead.getArrowHeadFromString(head); + }catch(IOException e){ + throw e; + } + currentHeads.put(nodesId.get(id), headShape); + int headDescriptionIndex = Edge.NO_END_DESCRIPTION_INDEX; + for(int j=0; j<heads.length;j++){ + if(heads[j].equals(headShape)){ + headDescriptionIndex = j; + break; + } + } + setEndDescription(nodesId.get(id),headDescriptionIndex); + } + } + } + + @Override + public int getStipplePattern(){ + int result = 0; + switch(getStyle()){ + case Solid : result = 0xFFFF; + break; + case Dashed : result = 0xF0F0; + break; + case Dotted : result = 0xAAAA ; + break; + } + return result; + } + + @Override + public Object clone(){ + return new SimpleShapeEdge(getType(), getStyle(), heads, getAvailableEndDescriptions(), getMinAttachedNodes(), getMaxAttachedNodes() ); + } + + + + private ArrowHead[] heads; + private Map<Node,ArrowHead> currentHeads; + private static InputStream straightSound; + private static InputStream dottedSound; + private static InputStream dashedSound; + + static{ + Class<SimpleShapeEdge> c = SimpleShapeEdge.class; + straightSound = c.getResourceAsStream("audio/straightLine.mp3"); + dottedSound = c.getResourceAsStream("audio/dashedLine.mp3"); + dashedSound = c.getResourceAsStream("audio/dottedLine.mp3"); + SoundFactory.getInstance().loadSound(straightSound); + SoundFactory.getInstance().loadSound(dottedSound); + SoundFactory.getInstance().loadSound(dashedSound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,584 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RectangularShape; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; +import uk.ac.qmul.eecs.ccmi.utils.Pair; + +/** + * + * A diagram node that can be represented visually as a simple shape such as + * a rectangle, square, circle, ellipse or triangle. + * + */ +@SuppressWarnings("serial") +public abstract class SimpleShapeNode extends Node { + + public static SimpleShapeNode getInstance(ShapeType shapeType, String typeName, NodeProperties properties){ + switch(shapeType){ + case Rectangle : + return new RectangularNode(typeName, properties); + case Square : + return new SquareNode(typeName, properties); + case Circle : + return new CircleNode(typeName, properties); + case Ellipse : + return new EllipticalNode(typeName, properties); + case Triangle : + return new TriangularNode(typeName, properties); + } + return null; + } + + protected SimpleShapeNode(String typeName, NodeProperties properties){ + super(typeName, properties); + dataDisplayBounds = (Rectangle2D.Double)getMinBounds(); + /* Initialise the data structures for displaying the properties inside and outside */ + propertyNodesMap = new LinkedHashMap<String,List<PropertyNode>>(); + int numInsideProperties = 0; + for(String type : getProperties().getTypes()){ + if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside) + numInsideProperties++; + else + propertyNodesMap.put(type, new LinkedList<PropertyNode>()); + } + propertyLabels = new MultiLineString[numInsideProperties]; + nameLabel = new MultiLineString(); + } + + @Override + protected void notifyChange(ElementChangedEvent evt){ + if(!evt.getChangeType().equals("translate")&&!evt.getChangeType().equals("stop_move")) //don't reshape for just moving + reshape(); + super.notifyChange(evt); + } + + @Override + public void setId(long id){ + super.setId(id); + /* when they are given an id nodes change name into "new <type> node <id>" * + * where <type> is the actual type of the node and <id> is the given id * + * therefore a reshape is necessary to display the new name */ + Rectangle2D boundsBeforeReshape = getBounds(); + reshape(); + /* the reshape might change the bounds, so the shape is translated so that the top-left * + * point is at the same position as before just to keep it more consistent */ + Rectangle2D boundsAfterReshape = getBounds(); + translateImplementation( + new Point2D.Double(), + boundsBeforeReshape.getX() - boundsAfterReshape.getX(), + boundsBeforeReshape.getY() - boundsAfterReshape.getY() + ); + } + + @Override + public boolean contains(Point2D p) { + if (getShape().contains(p)) + return true; + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)) + return true; + return false; + } + + protected void reshape(){ + Pair<List<String>, List<String>> splitPropertyTypes = splitPropertyTypes(); + /* properties displayed internally */ + reshapeInnerProperties(splitPropertyTypes.first); + /* properties displayed externally */ + reshapeOuterProperties(splitPropertyTypes.second); + } + + protected Pair<List<String>, List<String>> splitPropertyTypes(){ + List<String> types = getProperties().getTypes(); + ArrayList<String> insidePropertyTypes = new ArrayList<String>(types.size()); + ArrayList<String> outsidePropertyTypes = new ArrayList<String>(types.size()); + for(String type : types){ + if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside) + insidePropertyTypes.add(type); + else + outsidePropertyTypes.add(type); + } + + return new Pair<List<String>, List<String>> (insidePropertyTypes,outsidePropertyTypes); + } + + protected void reshapeOuterProperties(List<String> outsidePropertyTypes){ + for(String type : outsidePropertyTypes){ + List<PropertyNode> propertyNodes = propertyNodesMap.get(type); + List<String> propertyValues = getProperties().getValues(type); + int diff = propertyNodes.size()-propertyValues.size(); + if(diff > 0) // properties have been removed + for(int i=0; i < diff; i++) + propertyNodes.remove(propertyNodes.size() - 1); + else if(diff < 0){ // properties have been added. We need more properties node. + for(int i=0; i < -diff; i++){ + PropertyNode propertyNode = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType()); + Rectangle2D bounds = getBounds(); + double x = bounds.getCenterX() - bounds.getWidth()/2 - PROP_NODE_DIST; + double y = bounds.getCenterX() - PROP_NODE_DIST * i; + propertyNode.translate(x, y); + propertyNodes.add(propertyNode); + } + } + /* set the text on all the property nodes */ + int i = 0; + for(String text : propertyValues){ + NodeProperties.Modifiers modifiers = getProperties().getModifiers(type); + Set<Integer> viewIndexes = modifiers.getIndexes(i); + ModifierView[] views = new ModifierView[viewIndexes.size()]; + int j =0; + for(Integer I : viewIndexes){ + views[j] = (ModifierView) getProperties().getModifiers(type).getView(modifiers.getTypes().get(I)); + j++; + } + propertyNodes.get(i).setText(text,views); + i++; + } + } + } + + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + /* set the bounds for each multiline string and the resulting bound of the node */ + nameLabel = new MultiLineString(); + nameLabel.setText(getName().isEmpty() ? " " : getName()); + nameLabel.setBold(true); + Rectangle2D r = nameLabel.getBounds(); + + for(int i=0; i<insidePropertyTypes.size();i++){ + propertyLabels[i] = new MultiLineString(); + String propertyType = insidePropertyTypes.get(i); + if(getProperties().getValues(propertyType).size() == 0){ + propertyLabels[i].setText(" "); + }else{ + propertyLabels[i].setJustification(MultiLineString.LEFT); + String[] array = new String[getProperties().getValues(propertyType).size()]; + propertyLabels[i].setText(getProperties().getValues(propertyType).toArray(array), getProperties().getModifiers(propertyType)); + } + r.add(new Rectangle2D.Double(r.getX(),r.getMaxY(),propertyLabels[i].getBounds().getWidth(),propertyLabels[i].getBounds().getHeight())); + } + /* set a gap to uniformly distribute the extra space among property rectangles to reach the minimum bound's height */ + boundsGap = 0; + Rectangle2D.Double minBounds = (Rectangle2D.Double)getMinBounds(); + if(r.getHeight() < minBounds.height){ + boundsGap = minBounds.height - r.getHeight(); + boundsGap /= insidePropertyTypes.size(); + } + r.add(minBounds); //make sure it's at least as big as the minimum bounds + dataDisplayBounds.setFrame(new Rectangle2D.Double(dataDisplayBounds.x,dataDisplayBounds.y,r.getWidth(),r.getHeight())); + } + + /** + Draw the node from the Shape with shadow + @param g2 the graphics context + */ + @Override + public void draw(Graphics2D g2){ + /* draw the external shape */ + Shape shape = getShape(); + if (shape == null) return; + Color oldColor = g2.getColor(); + g2.translate(SHADOW_GAP, SHADOW_GAP); + g2.setColor(SHADOW_COLOR); + g2.fill(shape); + g2.translate(-SHADOW_GAP, -SHADOW_GAP); + g2.setColor(g2.getBackground()); + g2.fill(shape); + g2.setColor(Color.BLACK); + g2.draw(shape); + g2.setColor(oldColor); + + /* if there ain't any property to display inside, then display the name in the middle of the data Display bounds */ + if(!anyInsideProperties()){ + nameLabel.draw(g2, dataDisplayBounds); + }else{ + /* draw name */ + Rectangle2D currentBounds = new Rectangle2D.Double( + dataDisplayBounds.x, + dataDisplayBounds.y, + dataDisplayBounds.getWidth(), + nameLabel.getBounds().getHeight()); + if(drawPropertySeparators){ + Shape oldClip = g2.getClip(); + g2.setClip(getShape()); + g2.draw(new Rectangle2D.Double( + getBounds().getX(), + dataDisplayBounds.y, + getBounds().getWidth(), + nameLabel.getBounds().getHeight()) + ); + g2.setClip(oldClip); + } + nameLabel.draw(g2, currentBounds); + + /* draw internal properties */ + Rectangle2D previousBounds; + for(int i=0;i<propertyLabels.length;i++){ + previousBounds = currentBounds; + currentBounds = new Rectangle2D.Double( + previousBounds.getX(), + previousBounds.getMaxY(), + dataDisplayBounds.getWidth(), + propertyLabels[i].getBounds().getHeight()+boundsGap); + if(drawPropertySeparators){ + Shape oldClip = g2.getClip(); + g2.setClip(getShape()); + g2.draw(new Rectangle2D.Double( + getBounds().getX(), + currentBounds.getY(), + getBounds().getWidth(), + currentBounds.getHeight()) + ); + g2.setClip(oldClip); + } + propertyLabels[i].draw(g2, currentBounds); + } + } + + /* draw external properties */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList){ + pn.draw(g2); + Direction d = new Direction( getBounds().getCenterX() - pn.getCenter().getX(), getBounds().getCenterY() - pn.getCenter().getY()); + g2.draw(new Line2D.Double(pn.getConnectionPoint(d), getConnectionPoint(d.turn(180)))); + } + /* draw visual cue for bookmarks and notes */ + super.draw(g2); + } + + protected Rectangle2D getMinBounds(){ + return (Rectangle2D)minBounds.clone(); + } + + public abstract ShapeType getShapeType(); + + @Override + public void encode(Document doc, Element parent){ + super.encode(doc, parent); + if(getProperties().isEmpty()) + return; + NodeList propTagList = doc.getElementsByTagName(PersistenceManager.PROPERTY); + + /* scan all the PROPERTY tags to add the position tag */ + for(int i = 0 ; i< propTagList.getLength(); i++){ + Element propertyTag = (Element)propTagList.item(i); + Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0); + String type = typeTag.getTextContent(); + + /* a property of another node, continue */ + if(!getProperties().getTypes().contains(type)) + continue; + + if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside) + continue; + + List<String> values = getProperties().getValues(type); + if(values.isEmpty()) + continue; + + NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT); + List<PropertyNode> pnList = propertyNodesMap.get(type); + for(int j=0; j<elementTagList.getLength();j++){ + Element elementTag = (Element)elementTagList.item(j); + Element positionTag = doc.createElement(SimpleShapePrototypePersistenceDelegate.POSITION); + positionTag.setAttribute(PersistenceManager.X, String.valueOf(pnList.get(j).getX())); + positionTag.setAttribute(PersistenceManager.Y, String.valueOf(pnList.get(j).getY())); + elementTag.appendChild(positionTag); + } + } + } + + @Override + public void decode(Document doc, Element nodeTag) throws IOException{ + super.decode(doc, nodeTag); + + NodeList propTagList = nodeTag.getElementsByTagName(PersistenceManager.PROPERTY); + + /* split the property types into internal and external, properties have been set by super.decodeXMLInstance */ + ArrayList<String> insidePropertyTypes = new ArrayList<String>(getProperties().getTypes().size()); + ArrayList<String> outsidePropertyTypes = new ArrayList<String>(getProperties().getTypes().size()); + for(String type : getProperties().getTypes()){ + if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside) + insidePropertyTypes.add(type); + else + outsidePropertyTypes.add(type); + } + + /* set the multi-line string bounds for the properties which are displayed internally */ + reshapeInnerProperties(insidePropertyTypes); + + /* scan all the PROPERTY tags to decode the position tag of the properties which are displayed externally */ + for(int i = 0 ; i< propTagList.getLength(); i++){ + Element propertyTag = (Element)propTagList.item(i); + if(propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null) + throw new IOException(); + Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0); + + String type = typeTag.getTextContent(); + /* (check on whether type exists in the node type definition is done in super.decode */ + + if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside) + continue; + /* this will create external nodes and assign them their position */ + NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT); + List<PropertyNode> pnList = new LinkedList<PropertyNode>(); + for(int j=0; j<elementTagList.getLength();j++){ + Element elementTag = (Element)elementTagList.item(j); + if(elementTag.getElementsByTagName(SimpleShapePrototypePersistenceDelegate.POSITION).item(0) == null || + elementTag.getElementsByTagName(PersistenceManager.VALUE).item(0) == null) + throw new IOException(); + Element positionTag = (Element)elementTag.getElementsByTagName(SimpleShapePrototypePersistenceDelegate.POSITION).item(0); + Element valueTag = (Element)elementTag.getElementsByTagName(PersistenceManager.VALUE).item(0); + double dx,dy; + try{ + dx = Double.parseDouble(positionTag.getAttribute(PersistenceManager.X)); + dy = Double.parseDouble(positionTag.getAttribute(PersistenceManager.Y)); + }catch(NumberFormatException nfe){ + throw new IOException(); + } + PropertyNode pn = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType()); + pn.translate(dx, dy); + pn.setText(valueTag.getTextContent(),null); + pnList.add(pn); + } + propertyNodesMap.put(type, pnList); + /* this will apply the modifier format to the properties */ + reshapeOuterProperties(outsidePropertyTypes); + } + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + dataDisplayBounds.setFrame(dataDisplayBounds.getX() + dx, + dataDisplayBounds.getY() + dy, + dataDisplayBounds.getWidth(), + dataDisplayBounds.getHeight()); + /* translate all the external property nodes */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + pn.translate(dx, dy); + } + + @Override + public Object clone(){ + SimpleShapeNode n = (SimpleShapeNode)super.clone(); + n.propertyLabels = new MultiLineString[propertyLabels.length]; + n.nameLabel = new MultiLineString(); + n.propertyNodesMap = new LinkedHashMap<String, List<PropertyNode>>(); + for(String s : propertyNodesMap.keySet()) + n.propertyNodesMap.put(s, new LinkedList<PropertyNode>()); + n.dataDisplayBounds = (Rectangle2D.Double)dataDisplayBounds.clone(); + return n; + } + + protected boolean anyInsideProperties(){ + boolean propInside = false; + for(String type : getProperties().getTypes()){ + if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside){ + if(!getProperties().getValues(type).isEmpty()){ + propInside = true; + break; + } + } + } + return propInside; + } + + protected Rectangle2D.Double dataDisplayBounds; + protected double boundsGap; + protected boolean drawPropertySeparators = true; + protected MultiLineString[] propertyLabels; + protected MultiLineString nameLabel; + protected Map<String,List<PropertyNode>> propertyNodesMap; + + public static enum ShapeType {Circle, Ellipse, Rectangle, Square, Triangle, Transparent}; + public static enum Position {Inside, Outside}; + + private static final int DEFAULT_WIDTH = 100; + private static final int DEFAULT_HEIGHT = 60; + private static final Rectangle2D.Double minBounds = new Rectangle2D.Double(0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT); + private static final int PROP_NODE_DIST = 50; + + protected static class PropertyNode{ + public PropertyNode(ShapeType aShape){ + /* add a little padding in the shape holding the label */ + label = new MultiLineString(){ + public Rectangle2D getBounds(){ + Rectangle2D bounds = super.getBounds(); + if(bounds.getWidth() != 0 || bounds.getHeight() != 0){ + bounds.setFrame( + bounds.getX(), + bounds.getY(), + bounds.getWidth() + PADDING, + bounds.getHeight() + PADDING); + } + return bounds; + } + }; + label.setJustification(MultiLineString.CENTER); + shapeType = aShape; + shape = label.getBounds(); + } + + public void setText(String text, ModifierView[] views){ + label.setText(text,views); + + switch(shapeType){ + case Circle : + Rectangle2D circleBounds = EllipticalNode.getOutBounds(label.getBounds()); + shape = new Ellipse2D.Double( + circleBounds.getX(), + circleBounds.getY(), + Math.max(circleBounds.getWidth(),circleBounds.getHeight()), + Math.max(circleBounds.getWidth(),circleBounds.getHeight()) + ); + break; + case Ellipse : + Rectangle2D ellipseBounds = EllipticalNode.getOutBounds(label.getBounds()); + shape = new Ellipse2D.Double( + ellipseBounds.getX(), + ellipseBounds.getY(), + ellipseBounds.getWidth(), + ellipseBounds.getHeight() + ); + break; + case Triangle : + shape = TriangularNode.getOutShape(label.getBounds()); + break; + default : // Rectangle, Square and Transparent + shape = label.getBounds();; + break; + } + + /* a new shape, placed at (0,0) has been created as a result of set text, therefore * + * we must put it back where the old shape was, since the translation is performed * + * by adding the translate args to x and y, x and y must first be set to 0 */ + double currentX = x; + double currentY = y; + x = 0; + y = 0; + translate(currentX,currentY); + } + + public void draw(Graphics2D g){ + Color oldColor = g.getColor(); + if(shapeType != ShapeType.Transparent){ + g.translate(SHADOW_GAP, SHADOW_GAP); + g.setColor(SHADOW_COLOR); + g.fill(shape); + g.translate(-SHADOW_GAP, -SHADOW_GAP); + + g.setColor(g.getBackground()); + g.fill(shape); + g.setColor(Color.BLACK); + g.draw(shape); + } + + label.draw(g, shape.getBounds2D()); + g.setColor(oldColor); + } + + public void translate(double dx, double dy){ + x += dx; + y += dy; + + if(shape instanceof Path2D){ //it's a triangle + Rectangle2D labelBounds = label.getBounds(); + labelBounds.setFrame( + x, + y, + labelBounds.getWidth(), + labelBounds.getHeight() + ); + shape = TriangularNode.getOutShape(labelBounds); + }else{ + Rectangle2D bounds = shape.getBounds2D(); + ((RectangularShape)shape).setFrame( + x, + y, + bounds.getWidth(), + bounds.getHeight() + ); + } + } + + public Point2D getConnectionPoint(Direction d) { + switch(shapeType){ + case Circle : + case Ellipse : + return EllipticalNode.calculateConnectionPoint(d, shape.getBounds2D()); + case Triangle : + return TriangularNode.calculateConnectionPoint(d, shape.getBounds2D()); + default : + return RectangularNode.calculateConnectionPoint(d, shape.getBounds2D()); + } + } + + public boolean contains(Point2D p){ + return shape.contains(p); + } + + public Point2D getCenter(){ + Rectangle2D bounds = shape.getBounds2D() ; + return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY()); + } + + double getX(){ + return x; + } + + double getY(){ + return y; + } + + private static final int PADDING = 5; + private MultiLineString label; + private ShapeType shapeType; + private Shape shape; + private double x; + private double y; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapePrototypePersistenceDelegate.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,258 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.LineStyle; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PrototypePersistenceDelegate; + +/** + * + * A PrototypePersistenceDelegate class which provides informations to be saved and restored by a PersistenceManager + * in order to rebuild simple shaped nodes and edges out of an XML file. + * + */ +public class SimpleShapePrototypePersistenceDelegate implements PrototypePersistenceDelegate{ + + public void encodeNodePrototype(Document doc, Element parent, Node node){ + SimpleShapeNode n = (SimpleShapeNode)node; + Element typeTag = doc.createElement(PersistenceManager.TYPE); + typeTag.appendChild(doc.createTextNode(n.getType())); + parent.appendChild(typeTag); + + Element shapeType = doc.createElement(SHAPE_TYPE); + shapeType.appendChild(doc.createTextNode(n.getShapeType().toString())); + parent.appendChild(shapeType); + + Element propertiesTag = doc.createElement(PersistenceManager.PROPERTIES); + parent.appendChild(propertiesTag); + for(String type : n.getProperties().getTypes()){ + Element propertyTag = doc.createElement(PersistenceManager.PROPERTY); + propertiesTag.appendChild(propertyTag); + + Element propertyTypeTag = doc.createElement(PersistenceManager.TYPE); + propertyTypeTag.appendChild(doc.createTextNode(type)); + propertyTag.appendChild(propertyTypeTag); + + Element viewTag = doc.createElement(VIEW); + propertyTag.appendChild(viewTag); + PropertyView view = (PropertyView)n.getProperties().getView(type); + viewTag.setAttribute(POSITION, view.getPosition().toString()); + viewTag.setAttribute(SHAPE_TYPE, view.getShapeType().toString()); + Modifiers modifiers = n.getProperties().getModifiers(type); + if(!modifiers.isNull()){ + Element modifiersTag = doc.createElement(PersistenceManager.MODIFIERS); + propertyTag.appendChild(modifiersTag); + for(int i=0; i<modifiers.getTypes().size();i++){ + String modifierType = modifiers.getTypes().get(i); + Element modifierTag = doc.createElement(PersistenceManager.MODIFIER); + modifiersTag.appendChild(modifierTag); + modifierTag.setAttribute(PersistenceManager.ID, String.valueOf(i)); + + Element modifierTypeTag = doc.createElement(PersistenceManager.TYPE); + modifierTag.appendChild(modifierTypeTag); + modifierTypeTag.appendChild(doc.createTextNode(modifierType)); + + ModifierView modifierView = (ModifierView)modifiers.getView(modifierType); + viewTag = doc.createElement(VIEW); + modifierTag.appendChild(viewTag); + viewTag.setAttribute(BOLD, modifierView.isBold()+""); + viewTag.setAttribute(UNDERLINE, modifierView.isUnderline()+""); + viewTag.setAttribute(ITALIC, modifierView.isItalic()+""); + viewTag.setAttribute(PREFIX, modifierView.getPrefix()); + viewTag.setAttribute(SUFFIX, modifierView.getSuffix()); + } + } + } + } + + public void encodeEdgePrototype(Document doc, Element parent, Edge edge){ + SimpleShapeEdge e = (SimpleShapeEdge)edge; + Element typeTag = doc.createElement(PersistenceManager.TYPE); + typeTag.appendChild(doc.createTextNode(e.getType())); + parent.appendChild(typeTag); + + Element lineStyleTag = doc.createElement(LINE_STYLE); + lineStyleTag.appendChild(doc.createTextNode(e.getStyle().toString())); + parent.appendChild(lineStyleTag); + + Element minNodesTag = doc.createElement(MIN_ATTACHED_NODES); + parent.appendChild(minNodesTag); + minNodesTag.appendChild(doc.createTextNode(e.getMinAttachedNodes()+"")); + + Element maxNodesTag = doc.createElement(MAX_ATTACHED_NODES); + parent.appendChild(maxNodesTag); + maxNodesTag.appendChild(doc.createTextNode(e.getMaxAttachedNodes()+"")); + + if(e.getHeads() != null) + if(e.getHeads().length != 0){ + Element headsTag = doc.createElement(HEADS); + parent.appendChild(headsTag); + for(int i=0; i<e.getHeads().length; i++){ + ArrowHead head = e.getHeads()[i]; + Element headTag = doc.createElement(HEAD); + headsTag.appendChild(headTag); + headTag.setAttribute(HEAD, head.toString()); + headTag.setAttribute(HEAD_DESCRIPTION, e.getAvailableEndDescriptions()[i]); + } + } + } + + public Node decodeNodePrototype(Element root) throws IOException{ + if(root.getElementsByTagName(PersistenceManager.TYPE).item(0) == null || + root.getElementsByTagName(SHAPE_TYPE).item(0) == null) + throw new IOException(); + + String typeName = root.getElementsByTagName(PersistenceManager.TYPE).item(0).getTextContent(); + String shapeTypeName = root.getElementsByTagName(SHAPE_TYPE).item(0).getTextContent(); + SimpleShapeNode.ShapeType shapeType; + try { + shapeType = SimpleShapeNode.ShapeType.valueOf(shapeTypeName); + }catch(IllegalArgumentException e){ + throw new IOException(); + } + + NodeList propertyTagList = root.getElementsByTagName(PersistenceManager.PROPERTY); + LinkedHashMap<String,Set<String>> propertyTypeDefinition = new LinkedHashMap<String,Set<String>>(); + Map<String, PropertyView> propertyViews = new LinkedHashMap<String, PropertyView>(); + Map<String, Map<String,ModifierView>> modifiersView = new LinkedHashMap<String, Map<String,ModifierView>>(); + + for(int i = 0 ; i< propertyTagList.getLength(); i++){ + Element property = (Element)propertyTagList.item(i); + if(property.getElementsByTagName(PersistenceManager.TYPE).item(0) == null || + property.getElementsByTagName(VIEW).item(0) == null) + throw new IOException(); + + String propertyType = property.getElementsByTagName(PersistenceManager.TYPE).item(0).getTextContent(); + Element viewTag = (Element) property.getElementsByTagName(VIEW).item(0); + viewTag.getAttributes(); + try{ + PropertyView propertyView = new PropertyView( + Enum.valueOf(SimpleShapeNode.Position.class,viewTag.getAttribute(POSITION)), + Enum.valueOf(SimpleShapeNode.ShapeType.class,viewTag.getAttribute(SHAPE_TYPE)) + ); + propertyViews.put(propertyType, propertyView); + }catch(IllegalArgumentException e){ + throw new IOException(e); + } + + NodeList modifierTagList = property.getElementsByTagName(PersistenceManager.MODIFIER); + Set<String> modifierTypeDefinition = null; + /* modifierViewsValue is the Map to be eventually put into the modifierViews as a value */ + /* (can be null), the key being the current property (the for cycle current index one) */ + Map<String,ModifierView> modifierViewsValue = null; + if(modifierTagList.getLength() > 0){ + modifierTypeDefinition = new LinkedHashSet<String>(); + modifierViewsValue = new LinkedHashMap<String,ModifierView>(); + } + for(int j=0; j<modifierTagList.getLength();j++ ){ + Element modifierTag = (Element)modifierTagList.item(j); + if(modifierTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null || + modifierTag.getElementsByTagName(VIEW).item(0) == null) + throw new IOException(); + String modifierType = modifierTag.getElementsByTagName(PersistenceManager.TYPE).item(0).getTextContent(); + modifierTypeDefinition.add(modifierType); + + Element modifierViewTag = (Element) modifierTag.getElementsByTagName(VIEW).item(0); + ModifierView modifierView = new ModifierView( + Boolean.parseBoolean(modifierViewTag.getAttribute(UNDERLINE)), + Boolean.parseBoolean(modifierViewTag.getAttribute(BOLD)), + Boolean.parseBoolean(modifierViewTag.getAttribute(ITALIC)), + modifierViewTag.getAttribute(PREFIX), + modifierViewTag.getAttribute(SUFFIX) + ); + modifierViewsValue.put(modifierType, modifierView); + } + if(modifierTagList.getLength() > 0){ + modifiersView.put(propertyType, modifierViewsValue); + } + propertyTypeDefinition.put(propertyType, modifierTypeDefinition); + } + + /* create the properties and set the views */ + NodeProperties prps = new NodeProperties(propertyTypeDefinition); + for(String propertyType : propertyViews.keySet()){ + prps.setView(propertyType, propertyViews.get(propertyType)); + if(modifiersView.get(propertyType) != null) + for(String modifierType : modifiersView.get(propertyType).keySet()){ + prps.getModifiers(propertyType).setView(modifierType, modifiersView.get(propertyType).get(modifierType)); + } + } + + return SimpleShapeNode.getInstance(shapeType, typeName, prps); + } + + public Edge decodeEdgePrototype(Element root) throws IOException{ + if(root.getElementsByTagName(PersistenceManager.TYPE).item(0) == null || + root.getElementsByTagName(LINE_STYLE).item(0) == null || + root.getElementsByTagName(MIN_ATTACHED_NODES).item(0) == null || + root.getElementsByTagName(MAX_ATTACHED_NODES).item(0) == null) + throw new IOException(); + String typeName = root.getElementsByTagName(PersistenceManager.TYPE).item(0).getTextContent(); + LineStyle lineStyle = null; + try{ + lineStyle = LineStyle.valueOf(root.getElementsByTagName(LINE_STYLE).item(0).getTextContent()); + }catch(IllegalArgumentException e){ + throw new IOException(e); + } + int minAttachedNodes = Integer.parseInt(root.getElementsByTagName(MIN_ATTACHED_NODES).item(0).getTextContent()); + int maxAttachedNodes = Integer.parseInt(root.getElementsByTagName(MAX_ATTACHED_NODES).item(0).getTextContent()); + + NodeList headTagList = root.getElementsByTagName(HEAD); + ArrowHead[] heads = new ArrowHead[headTagList.getLength()]; + String[] headDescriptions = new String[headTagList.getLength()]; + + for(int i=0;i<headTagList.getLength();i++){ + Element headTag = (Element)headTagList.item(i); + heads[i] = ArrowHead.getArrowHeadFromString(headTag.getAttribute(HEAD)); + headDescriptions[i] = headTag.getAttribute(HEAD_DESCRIPTION); + } + return new SimpleShapeEdge(typeName, lineStyle, heads, headDescriptions, minAttachedNodes, maxAttachedNodes); + } + + public static final String SHAPE_TYPE = "ShapeType"; + public static final String VIEW = "View"; + public static final String BOLD = "Bold"; + public static final String ITALIC = "Italic"; + public static final String UNDERLINE = "Underline"; + public static final String PREFIX = "Prefix"; + public static final String SUFFIX = "Suffix"; + public static final String POSITION = "Position"; + public static final String LINE_STYLE = "LineStyle"; + public static final String MIN_ATTACHED_NODES = "MinAttachedNodes"; + public static final String MAX_ATTACHED_NODES = "MaxAttachedNodes"; + public static final String HEADS = "Heads"; + public static final String HEAD = "Head"; + public static final String HEAD_DESCRIPTION = "headLabel"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleTemplateEditor.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,59 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Frame; +import java.util.Collection; +import java.util.ResourceBundle; + +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.TemplateEditor; + +/** + * + * The implementation of the TemplateEditor interface which uses a Wizard + * to allow the user to define the templates to be created. + * + */ +public class SimpleTemplateEditor implements TemplateEditor { + + @Override + public Diagram createNew(Frame frame, Collection<String> existingTemplates) { + Wizard wizard = new Wizard(frame,existingTemplates); + return wizard.execute(); + } + + @Override + public Diagram edit(Frame frame, Collection<String> existingTemplates, + Diagram diagram) { + Wizard wizard = new Wizard(frame,existingTemplates,diagram); + return wizard.execute(); + } + + @Override + public String getLabelForNew(){ + return ResourceBundle.getBundle(SpeechWizardDialog.class.getName()).getString("wizard_new_label"); + } + + @Override + public String getLabelForEdit(){ + return ResourceBundle.getBundle(SpeechWizardDialog.class.getName()).getString("wizard_edit_label"); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardDialog.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,94 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JRootPane; +import javax.swing.KeyStroke; +import javax.swing.event.ChangeEvent; + +import jwizardcomponent.dialog.SimpleJWizardDialog; + +/* + * The dialog where the template wizard is displayed + * + * @see Wizard + * + */ +@SuppressWarnings("serial") +public class SpeechWizardDialog extends SimpleJWizardDialog { + public SpeechWizardDialog(Frame owner){ + super(owner,true); + finished = false; + + ResourceBundle resources = ResourceBundle.getBundle(getClass().getName()); + setSize(350, 200); + setTitle(resources.getString("dialog.wizard.title")); + setLocationRelativeTo(owner); + + JButton button; + button = getWizardComponents().getNextButton(); + button.setText(resources.getString("button.next.label")); + button.getAccessibleContext().setAccessibleName(resources.getString("button.next.speech")); + + button = getWizardComponents().getBackButton(); + button.setText(resources.getString("button.previous.label")); + button.getAccessibleContext().setAccessibleName(resources.getString("button.previous.speech")); + + button = getWizardComponents().getCancelButton(); + button.setText(resources.getString("button.cancel.label")); + + button = getWizardComponents().getFinishButton(); + button.setText(resources.getString("button.finish.label")); + button.addChangeListener(new javax.swing.event.ChangeListener(){ + @Override + public void stateChanged(ChangeEvent e) { + ((JButton)e.getSource()).setEnabled(finished); + } + }); + JRootPane rootPane = getRootPane(); + rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "close"); + rootPane.getActionMap().put("close", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + dispose(); + } + }); + } + + /** + * Enables or disables the finish button. + * @param enabled + */ + public void setFinishButtonEnabled(boolean enabled){ + finished = enabled; + getWizardComponents().getFinishButton().setEnabled(true); + } + + private boolean finished; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardDialog.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,99 @@ +wizard_new_label=New Template +wizard_edit_label=Edit Template + +dialog.wizard.title=Template Creation Dialog +dialog.error.existing_value=Error: value {0} already exists +dialog.error.empty_text=Error: text cannot be empty +dialog.error.empty_desc=Error: {0} label cannot be empty +dialog.error.min_max=Error: minimum value cannot be greater than maximum value +dialog.summary.title=Template summary +dialog.summary.ok_button_label=Create Template +dialog.summary.cancel_button_label=Back to Editing +dialog.error.node_type_not_present=Node type {0} not present in template definition +dialog.error.edge_type_not_present=Edge type {0} not present in template definition + + +panel.home.title.new=What would you like to do? +panel.home.choice.diagram_name=Edit Diagram Name +panel.home.choice.nodes=Edit Diagram Nodes +panel.home.choice.edges=Edit Diagram Edges +panel.home.choice.finish=Finish Editing Template + +panel.diagram_name.title=Enter Diagram Name +panel.diagram_name.title.editing_existing_diagram=Enter Diagram Name (different from the starting diagram's) + +panel.nodes.title=Select Action to perform for Nodes +panel.nodes.actions.add=Add New Node +panel.nodes.actions.edit=Edit Existing Node +panel.nodes.actions.del=Delete Existing Node +panel.nodes.actions.finish=Finish Editing Nodes + +panel.node_edit.title=Select Node to edit +panel.node_del.title=Select Node to delete +panel.node_name.title=Enter Node name +panel.node_shape.title=Select Node shape + +panel.yesno_properties.title=Would you like to add any Properties? +panel.yesno_properties.add=Yes +panel.yesno_properties.finish=No + +panel.properties.title=Select Action to perform for Properties +panel.properties.actions.add=Add New Property +panel.properties.actions.edit=Edit Existing Property +panel.properties.actions.del=Delete Existing Property +panel.properties.action.finish=Finish Editing Properties + +panel.property_del.title=Select Property to delete +panel.property_edit.title=Select Property to edit +panel.property_name.title=Enter Property Name +panel.property_shape.title=Select property shape +panel.property_position.title=Where would you like to place the Property ? + +panel.yesno_modifiers.title=Would you like to add any Modifiers? +panel.yesno_modifiers.add=Yes +panel.yesno_modifiers.finish=No + +panel.modifiers.title=Select Action to perform for Modifiers +panel.modifiers.actions.add=Add New Modifier +panel.modifiers.actions.edit=Edit Existing Modifier +panel.modifiers.actions.del=Delete Existing Modifier +panel.modifiers.actions.finish=Finish Editing Modifiers + +panel.modifier_type.title=Enter Modifier Name +panel.modifier_del.title=Select Modifier to Delete +panel.modifier_edit.title=Select Modifier to Edit +panel.modifier_format.title=Select Modifier Format + +panel.edges.title=Select Action to perform for Edges +panel.edges.actions.add=Add New Edge +panel.edges.actions.edit=Edit Existing Edge +panel.edges.actions.del=Delete Existing Edge +panel.edges.actions.finish=Finish Editing Edges + +panel.edge_name.title=Enter Edge Name +panel.edge_del.title=Select Edge to Delete +panel.edge_edit.title=Select Edge to Edit +panel.edge_linestyle.title=Select Edge Line Style +panel.edge_min_nodes.title=Select Minimum Nodes To Connect +panel.edge_max_nodes.title=Select Maximum Nodes To Connect +panel.edge_yesno_arrow_head.title=Would you like to add arrow heads? +panel.edge_yesno_arrow_head.actions.add=Yes +panel.edge_yesno_arrow_head.actions.finish=No +panel.edge_arrow_head.title=Select arrow heads and set their labels + +modifier.format.bold=Bold +modifier.format.underline=Underline +modifier.format.italic=Italic +modifier.format.prefix=Prefix +modifier.format.suffix=Suffix + +button.next.label=Next > +button.next.speech=Next +button.previous.label=< Back +button.previous.speech=Back +button.finish.label=Done +button.cancel.label=Cancel + + +file={0}, File +dir={0}, Directory \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardPanel.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,319 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.ResourceBundle; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import jwizardcomponent.JWizardComponents; +import jwizardcomponent.JWizardPanel; +import uk.ac.qmul.eecs.ccmi.gui.LoopComboBox; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +/* + * The abstract class providing basic implementation for the panels displayed when the template + * wizard is run in order to build a diagram template. Subclasses will define the central component + * displayed in the panel. The central component is an input component (e.g. a JTextField), + * through which the user enters the input required for at that particular step of the wizard. + * + * + * @see Wizard + */ +@SuppressWarnings("serial") +abstract class SpeechWizardPanel extends JWizardPanel { + public SpeechWizardPanel(JWizardComponents wizardComponents, String title, int next, int previous){ + super(wizardComponents,title); + label = new JLabel(title); + this.next = next; + this.previous = previous; + } + + @Override + public void update(){ + Component focusOwner = assignFocus(); + NarratorFactory.getInstance().speak( + new StringBuilder(getPanelTitle()) + .append(' ') + .append(SpeechUtilities.getComponentSpeech(focusOwner)).toString()); + super.update(); + } + + @Override + public void setPanelTitle(String title){ + label.setText(title); + super.setPanelTitle(title); + } + + protected Component assignFocus(){ + if(component != null) + component.requestFocus(); + return component; + } + + /** + * Lays out the components according to the layout manager. This method is used by subclasses + * by passing the component the user use for input (e.g. a text field or a combo-box) as argument. + * such component is placed at the centre of the panel above the buttons. + * @param centralComponent the component to be laid out at the centre dialog + */ + protected void layoutComponents(JComponent centralComponent){ + component = centralComponent; + /* pressing enter on the central component results in a switch to the next panel */ + component.addKeyListener(new KeyAdapter(){ + @Override + public void keyPressed(KeyEvent evt){ + pressed = true; + } + + @Override + public void keyTyped(KeyEvent evt){ + /* switch on the next panel only if the press button started on the same window * + * this is to avoid keyTyped to be called after the panel switch and therefore refer * + * to a component different that the one the user pressed OK on */ + if(evt.getKeyChar() == '\n' && pressed) + getWizardComponents().getNextButton().doClick(); + pressed = false; + } + boolean pressed = false; + }); + + GridBagConstraints constr = new GridBagConstraints(); + constr.gridx = 0; + constr.gridy = 0; + constr.gridwidth = 1; + constr.gridheight = 1; + constr.weightx = 1.0; + constr.weighty = 0.0; + constr.anchor = GridBagConstraints.PAGE_START; + constr.fill = GridBagConstraints.BOTH; + constr.insets = new Insets(5, 5, 5, 5); + constr.ipadx = 0; + constr.ipady = 0; + + /* Label */ + setLayout(new GridBagLayout()); + JPanel labelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + label.setHorizontalAlignment(SwingConstants.LEADING); + labelPanel.add(label); + add(labelPanel,constr); + + /* JSeparator */ + constr.gridy = 1; + constr.anchor = GridBagConstraints.WEST; + constr.fill = GridBagConstraints.BOTH; + constr.insets = new Insets(1, 1, 1, 1); + add(new JSeparator(), constr); + + /* central component */ + Container centralComponentContainer; + if(centralComponent instanceof JScrollPane ){ + centralComponentContainer = centralComponent; + }else{ + centralComponentContainer = new JPanel(new GridBagLayout()); + centralComponentContainer.add(centralComponent + , new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0 + , GridBagConstraints.CENTER, GridBagConstraints.BOTH + , new Insets(0, 0, 0, 0), 0, 0)); + } + constr.gridy = 2; + constr.weighty = 1.0; + constr.anchor = GridBagConstraints.CENTER; + constr.insets = new Insets(0, 0, 0, 0); + add(centralComponentContainer,constr); + } + + @Override + public void next(){ + switchPanel(next); + } + + @Override + public void back(){ + switchPanel(previous); + } + + private JLabel label; + private int next; + private int previous; + private JComponent component; + public static int OWN_SWITCH = -1; + public static int DISABLE_SWITCH = -2; +} + +@SuppressWarnings("serial") +class SelectWizardPanel extends SpeechWizardPanel { + SelectWizardPanel(JWizardComponents wizardComponents, String title, Collection<String> options, int next, int previous, Model.Record record){ + super(wizardComponents,title,next,previous); + String[] optionsArray = new String[options.size()]; + comboBox = new LoopComboBox(new DefaultComboBoxModel(options.toArray(optionsArray))); + comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + layoutComponents(comboBox); + this.record = record; + } + + SelectWizardPanel(JWizardComponents wizardComponents, String title, Collection<String> options, int[] nexts, int previous, Model.Record record){ + this(wizardComponents, title, options, OWN_SWITCH, previous, record); + this.nexts = nexts; + } + + SelectWizardPanel(JWizardComponents wizardComponents, String title, Collection<String> options, int[] nexts, int previous){ + this(wizardComponents, title, options, nexts, previous,null); + } + + @Override + public void next(){ + if(record != null) + record.value = (String)comboBox.getSelectedItem(); + if(nexts != null) + switchPanel(nexts[comboBox.getSelectedIndex()]); + else + super.next(); + } + + @Override + public void update(){ + if(record != null) + comboBox.setSelectedItem(record.value); + super.update(); + } + + JComboBox comboBox; + int[] nexts; + Model.Record record; +} + +@SuppressWarnings("serial") +class TextWizardPanel extends SpeechWizardPanel { + TextWizardPanel(JWizardComponents wizardComponents, String title, Collection<String> existingValues, int next, int previous, Model.Record record){ + super(wizardComponents,title,next,previous); + textField = new JTextField(); + textField.setColumns(10); + textField.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + layoutComponents(textField); + this.record = record; + this.existingValues = existingValues; + } + + public void next(){ + String text = textField.getText().trim(); + /* if the user enters a text he has already entered (that is, it's in the existingValues the don't go on */ + /* and notify the user they have to chose another text. The only exception is when the record contains */ + /* the same text the user entered as that means they are going through the editing of an existing element*/ + if(text.isEmpty()||"\n".equals(text)){ + NarratorFactory.getInstance().speak(ResourceBundle.getBundle(SpeechWizardDialog.class.getName()).getString("dialog.error.empty_text")); + return; + } + for(String value : existingValues){ + if(value.equals(text) && !text.equals(record.value)){ + NarratorFactory.getInstance().speak(MessageFormat.format( + ResourceBundle.getBundle(SpeechWizardDialog.class.getName()).getString("dialog.error.existing_value"), + text)); + return; + } + } + if(record != null) + record.value = text; + super.next(); + } + + @Override + public void update(){ + if(record != null) + textField.setText(record.value); + super.update(); + } + + JTextField textField; + Collection<String> existingValues; + Model.Record record; +} + +@SuppressWarnings("serial") +class SpinnerWizardPanel extends SpeechWizardPanel{ + public SpinnerWizardPanel(JWizardComponents wizardComponents, String title, SpinnerModel spinnerModel, int next, int previous, Model.Record record){ + super(wizardComponents,title,next,previous); + this.record = record; + spinner = new JSpinner(spinnerModel); + spinner.addChangeListener(new ChangeListener(){ + @Override + public void stateChanged(ChangeEvent evt) { + JSpinner s = (JSpinner)(evt.getSource()); + NarratorFactory.getInstance().speak(s.getValue().toString()); + } + }); + JFormattedTextField tf = ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField(); + tf.setEditable(false); + tf.setFocusable(false); + tf.setBackground(Color.white); + layoutComponents(spinner); + } + + @Override + public void next(){ + if(record != null) + record.value = spinner.getValue().toString(); + super.next(); + } + + @Override + public void update(){ + if(record != null){ + if(!record.value.isEmpty()) + spinner.setValue(Integer.parseInt(record.value)); + } + super.update(); + } + + Model.Record record; + JSpinner spinner; +} + +@SuppressWarnings("serial") +class DummyWizardPanel extends JWizardPanel{ + DummyWizardPanel(JWizardComponents wizardComponents){ + super(wizardComponents); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SquareNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,115 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.InputStream; +import java.util.List; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + * A squared shaped diagram node. + * + */ +@SuppressWarnings("serial") +public class SquareNode extends RectangularNode { + + public SquareNode(String nodeType, NodeProperties properties){ + super(nodeType, properties); + dataDisplayBounds = getMinBounds(); + sqShape = getMinBounds(); + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + super.reshapeInnerProperties(insidePropertyTypes); + + double diffwh = dataDisplayBounds.getWidth() - dataDisplayBounds.getHeight(); + if(diffwh > 0){ + sqShape.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY()-diffwh/2,dataDisplayBounds.getWidth(),dataDisplayBounds.getWidth()); + } else if(diffwh < 0){ + sqShape.setFrame(dataDisplayBounds.getX()+diffwh/2,dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + }else{ + sqShape.setFrame(dataDisplayBounds); + } + } + + public Rectangle2D.Double getMinBounds(){ + Rectangle2D r = super.getMinBounds(); + r.setFrame(r.getX(), r.getY(), r.getHeight(), r.getHeight()); + return (Rectangle2D.Double)r; + } + + @Override + public ShapeType getShapeType(){ + return ShapeType.Square; + } + + @Override + public InputStream getSound(){ + return sound; + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + + sqShape.setFrame(sqShape.getX() + dx, + sqShape.getY() + dy, + sqShape.getWidth(), + sqShape.getHeight()); + super.translateImplementation(p,dx, dy); + } + + @Override + public Rectangle2D getBounds(){ + return (Rectangle2D)sqShape.clone(); + } + + @Override + public Shape getShape(){ + return sqShape; + } + + @Override + public Object clone(){ + SquareNode n = (SquareNode)super.clone(); + n.sqShape = (Rectangle2D.Double)sqShape.clone(); + return n; + } + private Rectangle2D.Double sqShape; + private static InputStream sound; + + static{ + sound = SquareNode.class.getResourceAsStream("audio/Square.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/TriangularNode.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,213 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.Rectangle2D.Double; +import java.io.InputStream; +import java.util.List; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + * A triangular shaped diagram node. + * + */ +@SuppressWarnings("serial") +public class TriangularNode extends SimpleShapeNode { + + + public TriangularNode(String typeName, NodeProperties properties) { + super(typeName, properties); + Rectangle2D dataBounds = getMinBounds(); + dataDisplayBounds.setFrame(dataBounds); + tShape = getOutShape(dataBounds); + /* by building the shape around dataBounds which was at (0,0) the new bounds */ + /* are now negative, so we need to bring the new bounds back at (0,0) */ + Rectangle2D bounds = getBounds(); + translateImplementation(new Point2D.Double(),0-bounds.getX(),0-bounds.getY()); + } + + @Override + protected Rectangle2D getMinBounds(){ + Rectangle2D minBounds = super.getMinBounds(); + return new Rectangle2D.Double(minBounds.getX(),minBounds.getY(),minBounds.getWidth()/2,minBounds.getHeight()/2); + } + + @Override + public ShapeType getShapeType() { + return ShapeType.Triangle; + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + super.translateImplementation(p,dx, dy); + tShape.transform(AffineTransform.getTranslateInstance(dx, dy)); + } + + public static Path2D.Double getOutShape(Rectangle2D r){ + Path2D.Double triangle = new Path2D.Double(GeneralPath.WIND_EVEN_ODD,3); + double minEdge = Math.min(r.getWidth(), r.getHeight()); + triangle.moveTo(r.getCenterX(), r.getY()-minEdge); + + double angle = Math.atan(minEdge/(r.getWidth()/2)); + double w = r.getHeight()/ Math.tan(angle); + triangle.lineTo(r.getX()-w, r.getMaxY()); + triangle.lineTo(r.getMaxX()+w, r.getMaxY()); + triangle.closePath(); + return triangle; + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + nameLabel = new MultiLineString(); + nameLabel.setText(getName().isEmpty() ? " " : getName()); + nameLabel.setBold(true); + + if(!super.anyInsideProperties()){ + dataDisplayBounds.setFrame(dataDisplayBounds.getX(), + dataDisplayBounds.getY(), + nameLabel.getBounds().getWidth(), + nameLabel.getBounds().getHeight()); + Rectangle2D minBounds = getMinBounds(); + dataDisplayBounds.add(new Rectangle2D.Double(dataDisplayBounds.getX(), dataDisplayBounds.getY(), minBounds.getWidth(),minBounds.getHeight())); + tShape = getOutShape(dataDisplayBounds); + }else { + Rectangle2D r = nameLabel.getBounds(); + + for(int i=0; i<insidePropertyTypes.size();i++){ + propertyLabels[i] = new MultiLineString(); + if(getProperties().getValues(insidePropertyTypes.get(i)).size() == 0){ + propertyLabels[i].setText(" "); + }else{ + propertyLabels[i].setJustification(MultiLineString.LEFT); + String[] a = new String[getProperties().getValues(insidePropertyTypes.get(i)).size()]; + propertyLabels[i].setText(getProperties().getValues(insidePropertyTypes.get(i)).toArray(a), getProperties().getModifiers(insidePropertyTypes.get(i))); + } + r.add(new Rectangle2D.Double(r.getX(),r.getMaxY(),propertyLabels[i].getBounds().getWidth(),propertyLabels[i].getBounds().getHeight())); + } + /* set a gap to uniformly distribute the extra space among property rectangles to reach the minimum bound's height */ + boundsGap = 0; + Rectangle2D.Double minBounds = (Rectangle2D.Double)getMinBounds(); + if(r.getHeight() < minBounds.height){ + boundsGap = minBounds.height - r.getHeight(); + boundsGap /= insidePropertyTypes.size(); + } + r.add(minBounds); + dataDisplayBounds.setFrame(new Rectangle2D.Double(dataDisplayBounds.x,dataDisplayBounds.y,r.getWidth(),r.getHeight())); + + tShape = getOutShape(dataDisplayBounds); + } + + } + + @Override + public Double getBounds() { + return (Double)tShape.getBounds2D(); + } + + @Override + public InputStream getSound(){ + return sound; + } + + @Override + public Point2D getConnectionPoint(Direction d) { + return calculateConnectionPoint(d,getBounds()); + } + + public static Point2D calculateConnectionPoint(Direction d, Rectangle2D bounds) { + if(d.getX() == 0){ + return new Point2D.Double(bounds.getCenterX(), + d.getY() > 0 ? bounds.getY() : bounds.getMaxY()); + } + + boolean left = false; + boolean right = false; + double dirTan = d.getY()/d.getX(); + double boundsTan = bounds.getHeight()/bounds.getWidth(); + double alfa = Math.atan(dirTan); + double alfaDegrees = Math.toDegrees(alfa); + + if(d.getY() < 0){ //from the top + if(alfaDegrees < 0) + right = true; + else + left = true; + }else{ //from the bottom + if(dirTan < boundsTan && d.getX() > 0) + right = true; + else if(dirTan > -boundsTan && d.getX() < 0) + left = true; + } + + if(right){ + double beta = Math.atan(bounds.getHeight()/(bounds.getWidth()/2)); + double py = bounds.getHeight()/2; + double x = py/ (Math.tan(alfa)-Math.tan(beta)); + double y = x * Math.tan(alfa); + return new Point2D.Double(bounds.getCenterX()-x, bounds.getCenterY()-y); + } + else if(left){ + double beta = - Math.atan(bounds.getHeight()/(bounds.getWidth()/2)); + double py = bounds.getHeight()/2; + double x = py/ (Math.tan(alfa)-Math.tan(beta)); + double y = x * Math.tan(alfa); + return new Point2D.Double(bounds.getCenterX()-x, bounds.getCenterY()-y); + } + else{ + return new Point2D.Double( + bounds.getCenterX() + ((bounds.getHeight()/2) * (d.getX()/d.getY()) ), + bounds.getMaxY()); + } + } + + @Override + public Shape getShape() { + return tShape; + } + + public Object clone(){ + return new TriangularNode(getType(),(NodeProperties)getProperties().clone()); + } + + private Path2D.Double tShape; + private static InputStream sound; + + static { + sound = TriangularNode.class.getResourceAsStream("audio/Triangle.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/Wizard.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,1209 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.AbstractAction; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SpinnerModel; + +import jwizardcomponent.FinishAction; +import jwizardcomponent.JWizardComponents; +import jwizardcomponent.JWizardPanel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.LineStyle; +import uk.ac.qmul.eecs.ccmi.gui.LoopComboBox; +import uk.ac.qmul.eecs.ccmi.gui.LoopSpinnerNumberModel; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.SpeechSummaryPane; +import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapeNode.ShapeType; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; +import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; + +/** + * + * A Wizard-like sequence of screens prompted to the user to let they input (e.g. which shape a node will have or how many nodes an edge can connect at most) + * how to build a template diagram. A template diagram is a prototype diagram + * (containing prototype nodes and edges) which can later on be used for creating instances + * of that type of diagram through clonation. The wizard is completely accessible via audio + * as all the content and all focused components names are spoken out by the {@link Narrator} through a text to speech synthesizer. + * + */ +public class Wizard { + public Wizard(Frame frame, Collection<String> existingDiagrams, Diagram diagramToEdit){ + dialog = new SpeechWizardDialog(frame); + resources = ResourceBundle.getBundle(SpeechWizardDialog.class.getName()); + + model = createModel(diagramToEdit); + node = new Model.Node(); + edge = new Model.Edge(); + property = new Model.Property(); + modifier = new Model.Modifier(); + + initWizardComponents(existingDiagrams,diagramToEdit); + + /* if the user is editing from an existing diagram, they have to choose a new name. They're switched * + * directly to the diagram name panel so they have to enter a new name as they would otherwise * + * not be allowed to proceed. */ + if(diagramToEdit != null) + dialog.getWizardComponents().setCurrentIndex(DIAGRAM_NAME); + + /* when the user clicks on the finish button they'll be prompted with a summary text area dialog * + * describing what they have created so far and asking for a confirmation to proceed with the actual * + * creation of the template. */ + dialog.getWizardComponents().setFinishAction(new FinishAction(dialog.getWizardComponents()){ + @Override + public void performAction(){ + String[] options = { + resources.getString("dialog.summary.ok_button_label"), + resources.getString("dialog.summary.cancel_button_label")}; + int result = SpeechSummaryPane.showDialog( + dialog, + resources.getString("dialog.summary.title"), + model.toString(), + SpeechSummaryPane.OK_CANCEL_OPTION, + options); + + if(result == SpeechSummaryPane.CANCEL){ // user wants to continue editing + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return; + } + /* create the actual diagram (it will be return to the client class by execute()) */ + diagram = createDiagram(model); + dialog.dispose(); + } + }); + } + + public Wizard(Frame frame, Collection<String> existingDiagrams){ + this(frame,existingDiagrams,null); + } + + public Diagram execute(){ + diagram = null; + dialog.show(); + if(diagram == null) + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return diagram; + } + + @SuppressWarnings("serial") + private void initWizardComponents(Collection<String> existingDiagrams, final Diagram diagramToEdit){ + /* --- MAIN PANEL --- */ + String[] choices = { + resources.getString("panel.home.choice.diagram_name"), + resources.getString("panel.home.choice.nodes"), + resources.getString("panel.home.choice.edges"), + }; + int[] nexts = {DIAGRAM_NAME,NODES,EDGES}; + + /* panel for the selection of main tasks when creating the diagram: enter diagram name, create node and * + * create edge. When a name is assigned an item is added to the selection which allows the user to finish * + * the template creation, much as they would do by pressing the finish button. If the user edits an existing * + * diagram they're prompted with a message to enter a new diagram name (as there cannot be two diagrams * + * with the same name. When the user enters the name the message goes away */ + add(HOME,new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.home.title.new"), + Arrays.asList(choices), + nexts, + SpeechWizardPanel.DISABLE_SWITCH + ){ + @Override + public void update(){ + if(!model.diagramName.value.isEmpty()){ + dialog.setFinishButtonEnabled(true); + /* if the diagram has a name the template creation can finish. So add a selection item to the * + * comboBox unless it's already there from a previous update (item count < 4 ) */ + if(comboBox.getItemCount() < 4) + ((DefaultComboBoxModel)comboBox.getModel()).addElement(resources.getString("panel.home.choice.finish")); + } + super.update(); + } + @Override + public void next(){ + if(comboBox.getSelectedIndex() == 3) + dialog.getWizardComponents().getFinishButton().doClick(); + else + super.next(); + } + }); + + /* --- DIAGRAM NAME INPUT PANEL --- */ + add(DIAGRAM_NAME, new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString(diagramToEdit == null ? "panel.diagram_name.title" : "panel.diagram_name.title.editing_existing_diagram"), + existingDiagrams, + HOME, + HOME, + model.diagramName + ){ + @Override + public void update(){ + /* this is a little nasty trick to achieve the following: when the user creates a new diagram out of an already existing + * one they're directly prompted with this panel. We want the name of the diagram to be there for awareness + * but at the same time it must not be accepted by the program as it would otherwise clash with + * with the starting diagam's. As the program accepts it when the text entered in the text field is equal to + * model.diagramName.value (for when the user wants to re-edit the name of a diagram they're creating) we must + * fill model.DiagramName.value with the name of the starting diagram to get it shown and spoken out but then + * it's assigned the empty string not to let the user to go forward */ + if(diagramToEdit != null && model.diagramName.value.isEmpty()){ + model.diagramName.value = diagramToEdit.getName(); + super.update(); + model.diagramName.value = ""; + }else{ + super.update(); + } + } + }); + + /* --- NODE ACTION SELECTION PANEL --- */ + /* decide whether to add a new node or to edit/delete an existing node */ + String[] nodeOptions = { + resources.getString("panel.nodes.actions.add"), + resources.getString("panel.nodes.actions.edit"), + resources.getString("panel.nodes.actions.del"), + resources.getString("panel.nodes.actions.finish")}; + int[] nodeNexts = {NODE_TYPE,NODE_EDIT,NODE_DEL,HOME}; + add(NODES, new ActionChooserPanel( + dialog.getWizardComponents(), + model.nodes.getNames(), + resources.getString("panel.nodes.title"), + nodeOptions, + nodeNexts, + HOME, + node + )); + + /* --- NODE TYPE NAME INPUT PANEL --- */ + add(NODE_TYPE,new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.node_name.title"), + model.nodes.getNames(), + NODE_SHAPE, + NODES, + node.type + )); + + /* --- NODE TO DELETE SELECTION PANEL*/ + add(NODE_DEL, new DeletePanel( + dialog.getWizardComponents(), + resources.getString("panel.node_del.title"), + NODES, + NODES, + model.nodes)); + + /* -- NODE TO EDIT SELECTION PANEL */ + add(NODE_EDIT, new EditPanel( + dialog.getWizardComponents(), + resources.getString("panel.node_edit.title"), + NODE_TYPE, + NODES, + model.nodes, + node + )); + + ShapeType[] shapeTypes = ShapeType.values(); + ArrayList<String> shapeTypeNames = new ArrayList<String>(shapeTypes.length); + for(int i=0; i<shapeTypes.length;i++) + if(shapeTypes[i] != ShapeType.Transparent) + shapeTypeNames.add(shapeTypes[i].toString()); + + /* -- NODE SHAPE SELECTION PANEL --- */ + add(NODE_SHAPE, new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.node_shape.title"), + shapeTypeNames, + NODE_YESNO_PROPERTIES, + NODE_TYPE, + node.shape + )); + + /* --- SELECT WHETHER THE THE NODE HAS TO HAVE PROPERTIES --- */ + String[] yesnoPropertyOptions = { + resources.getString("panel.yesno_properties.add"), + resources.getString("panel.yesno_properties.finish") + }; + int[] yesnoPropertyNexts = {PROPERTIES,NODES}; + + add(NODE_YESNO_PROPERTIES,new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.yesno_properties.title"), + Arrays.asList(yesnoPropertyOptions), + yesnoPropertyNexts, + NODE_SHAPE + ){ + @Override + public void next(){ + if(comboBox.getSelectedIndex() == 1){ + Model.Node newNode = new Model.Node(); + Model.copy(node, newNode); + model.nodes.put(newNode.id,newNode); + } + super.next(); + } + }); + + /* --- PROPERTIES ACTION SELECTION PANEL --- */ + String[] propertyOptions = { + resources.getString("panel.properties.actions.add"), + resources.getString("panel.properties.actions.edit"), + resources.getString("panel.properties.actions.del"), + resources.getString("panel.properties.action.finish")}; + int[] propertyNexts = {PROPERTY_TYPE,PROPERTY_EDIT,PROPERTY_DEL,NODES}; + + add(PROPERTIES, new ActionChooserPanel( + dialog.getWizardComponents(), + node.properties.getNames(), + resources.getString("panel.properties.title"), + propertyOptions, + propertyNexts, + NODE_SHAPE, + property + ){ + @Override + public void next(){ + /* if the user selects finish, create a new node put in it the values of */ + /* the temporary property and store it in the model */ + if( (comboBox.getSelectedIndex() == 1 && comboBox.getItemCount() == 2)|| + (comboBox.getSelectedIndex() == 3 && comboBox.getItemCount() == 4)){ + Model.Node newNode = new Model.Node(); + Model.copy(node, newNode); + model.nodes.put(newNode.id,newNode); + } + super.next(); + } + }); + + /* --- PROPERTY TYPE NAME INPUT PANEL --- */ + add(PROPERTY_TYPE,new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.property_name.title"), + node.properties.getNames(), + PROPERTY_POSITION, + PROPERTIES, + property.type + )); + + /* --- PROPERTY TO DELETE SELECTION PANEL --- */ + add(PROPERTY_DEL, new DeletePanel( + dialog.getWizardComponents(), + resources.getString("panel.property_del.title"), + PROPERTIES, + PROPERTIES, + node.properties + )); + + /* --- PROPERTY TO EDIT SELECTION PANEL --- */ + add(PROPERTY_EDIT, new EditPanel( + dialog.getWizardComponents(), + resources.getString("panel.property_edit.title"), + PROPERTY_TYPE, + PROPERTIES, + node.properties, + property + )); + + /* --- PROPERTY POSITION SELECTION DIALOG --- */ + SimpleShapeNode.Position positions[] = SimpleShapeNode.Position.values(); + ArrayList<String> positionNames = new ArrayList<String>(positions.length); + for(int i=0; i<positions.length;i++) + positionNames.add(positions[i].toString()); + int[] positionNexts = {PROPERTY_YESNO_MODIFIER,PROPERTY_SHAPE}; + + + add(PROPERTY_POSITION, new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.property_position.title"), + positionNames, + positionNexts, + PROPERTY_TYPE, + property.position + )); + + /* --- PROPERTY SHAPE SELECTION DIALOG --- */ + shapeTypeNames.add(ShapeType.Transparent.toString()); + add(PROPERTY_SHAPE, new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.property_shape.title"), + shapeTypeNames, + PROPERTY_YESNO_MODIFIER, + PROPERTY_POSITION, + property.shape + )); + + /* --- SELECT WHETHER THE THE PROPERTY HAS TO HAVE MODIFIERS --- */ + String[] yesnoModifierOptions = { + resources.getString("panel.yesno_modifiers.add"), + resources.getString("panel.yesno_modifiers.finish") + }; + int[] yesnoModifierNexts = {MODIFIERS,PROPERTIES}; + + add(PROPERTY_YESNO_MODIFIER,new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.yesno_modifiers.title"), + Arrays.asList(yesnoModifierOptions), + yesnoModifierNexts, + PROPERTY_POSITION + ){ + @Override + public void next(){ + if(comboBox.getSelectedIndex() == 1){ + Model.Property newProperty = new Model.Property(); + Model.copy(property, newProperty); + node.properties.put(newProperty.id,newProperty); + } + super.next(); + } + }); + /* --- MODIFIERS ACTION SELECTION PANE --- */ + String[] modifierOptions = { + resources.getString("panel.modifiers.actions.add"), + resources.getString("panel.modifiers.actions.edit"), + resources.getString("panel.modifiers.actions.del"), + resources.getString("panel.modifiers.actions.finish") + }; + int[] modifiersNexts = {MODIFIER_TYPE,MODIFIER_EDIT,MODIFIER_DEL,PROPERTIES}; + + add(MODIFIERS, new ActionChooserPanel( + dialog.getWizardComponents(), + property.modifiers.getNames(), + resources.getString("panel.modifiers.title"), + modifierOptions, + modifiersNexts, + PROPERTY_POSITION, + modifier){ + + @Override + public void next(){ + /* if the user selects finish, create a new property put in it the values of */ + /* the temporary property and store it in the model */ + if( (comboBox.getSelectedIndex() == 1 && comboBox.getItemCount() == 2)|| + (comboBox.getSelectedIndex() == 3 && comboBox.getItemCount() == 4)){ + Model.Property newProperty = new Model.Property(); + Model.copy(property, newProperty); + node.properties.put(newProperty.id,newProperty); + } + super.next(); + } + }); + + /* --- MODIFIER TYPE PANEL --- */ + add(MODIFIER_TYPE, new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.modifier_type.title"), + property.modifiers.getNames(), + MODIFIER_FORMAT, + MODIFIERS, + modifier.type + )); + + add(MODIFIER_DEL, new DeletePanel( + dialog.getWizardComponents(), + resources.getString("panel.modifier_del.title"), + MODIFIERS, + MODIFIERS, + property.modifiers)); + + add(MODIFIER_EDIT, new EditPanel( + dialog.getWizardComponents(), + resources.getString("panel.modifier_edit.title"), + MODIFIER_TYPE, + MODIFIERS, + property.modifiers, + modifier + )); + + add(MODIFIER_FORMAT, new FormatWizardPanel()); + /* --- EDGE ACTION SELECTION PANEL --- */ + /* decide whether to add a new edge or to edit/delete an existing edge */ + String[] edgeOptions = { + resources.getString("panel.edges.actions.add"), + resources.getString("panel.edges.actions.edit"), + resources.getString("panel.edges.actions.del"), + resources.getString("panel.edges.actions.finish")}; + int[] edgeNexts = {EDGE_TYPE,EDGE_EDIT,EDGE_DEL,HOME}; + add(EDGES, new ActionChooserPanel( + dialog.getWizardComponents(), + model.edges.getNames(), + resources.getString("panel.edges.title"), + edgeOptions, + edgeNexts, + HOME, + edge + )); + + /* --- EDGE TYPE NAME INPUT PANEL --- */ + add(EDGE_TYPE,new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_name.title"), + model.edges.getNames(), + EDGE_LINE_STYLE, + EDGES, + edge.type + )); + + /* --- EDGE TO DELETE SELECTION PANEL --- */ + add(EDGE_DEL, new DeletePanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_del.title"), + EDGES, + EDGES, + model.edges)); + + /* --- EDGE TO EDIT SELECTION PANEL --- */ + add(EDGE_EDIT, new EditPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_edit.title"), + EDGE_TYPE, + EDGES, + model.edges, + edge + )); + + /* --- LINE STYLE SELECTION PANEL --- */ + LineStyle[] lineStyles = LineStyle.values(); + String[] lineStyleNames = new String[lineStyles.length]; + for(int i=0; i<lineStyles.length;i++) + lineStyleNames[i] = lineStyles[i].toString(); + + add(EDGE_LINE_STYLE, new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_linestyle.title"), + Arrays.asList(lineStyleNames), + EDGE_MIN_NODES, + EDGE_TYPE, + edge.lineStyle + )); + + /* --- MIN NODES SELECTION PANEL --- */ + SpinnerModel minNodesSpinnerModel = new LoopSpinnerNumberModel(2,2,4); + add(EDGE_MIN_NODES,new SpinnerWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_min_nodes.title"), + minNodesSpinnerModel, + EDGE_MAX_NODES, + EDGE_LINE_STYLE, + edge.minNodes + )); + + /* --- MAX NODES SELECTION PANEL --- */ + SpinnerModel maxNodesSpinnerModel = new LoopSpinnerNumberModel(2,2,4); + add(EDGE_MAX_NODES, new SpinnerWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_max_nodes.title"), + maxNodesSpinnerModel, + EDGE_YESNO_ARROW_HEAD, + EDGE_MIN_NODES, + edge.maxNodes + ){ + @Override + public void next(){ + int min = Integer.parseInt(edge.minNodes.value); + int max = Integer.parseInt(spinner.getValue().toString()); + if(min > max){ + NarratorFactory.getInstance().speak(resources.getString("dialog.error.min_max")); + }else{ + super.next(); + } + } + + }); + + /* --- SELECT WHETHER THE EDGE MUST HAVE ARROW HEADS OR NOT --- */ + String[] arrowHeadOptions = { + resources.getString("panel.edge_yesno_arrow_head.actions.add"), + resources.getString("panel.edge_yesno_arrow_head.actions.finish") + }; + int[] arrowHeadNexts = {EDGE_ARROW_HEAD,EDGES}; + add(EDGE_YESNO_ARROW_HEAD,new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_yesno_arrow_head.title"), + Arrays.asList(arrowHeadOptions), + arrowHeadNexts, + EDGE_MAX_NODES + ){ + @Override + public void next(){ + if(comboBox.getSelectedIndex() == 1){ + Model.Edge newEdge = new Model.Edge(); + Model.copy(edge, newEdge); + model.edges.put(newEdge.id,newEdge); + } + super.next(); + } + }); + + /* --- ARROW HEAD SELECTION PANEL --- */ + add(EDGE_ARROW_HEAD, new ArrowHeadPanel()); + + add(LAST_PANEL, new DummyWizardPanel(dialog.getWizardComponents())); + + SpeechUtilities.changeTabListener((JComponent)dialog.getContentPane(), dialog); + } + + private void add(int index, JWizardPanel panel){ + dialog.getWizardComponents().addWizardPanel(index,panel); + } + + private Diagram createDiagram(Model model){ + /* create the node prototypes */ + Node[] nodes = new Node[model.nodes.size()]; + int i = 0; + for(Model.Node n : model.nodes.values()){ + nodes[i] = createDiagramNode(n); + i++; + } + /* create the edge prototypes */ + Edge[] edges = new Edge[model.edges.size()]; + i = 0; + for(Model.Edge e : model.edges.values()){ + edges[i] = createDiagramEdge(e); + i++; + } + return Diagram.newInstance(model.diagramName.value, nodes, edges, new SimpleShapePrototypePersistenceDelegate()); + } + + private Node createDiagramNode(Model.Node n){ + /* set up the properties */ + LinkedHashMap<String,Set<String>> propertiesTypeDefinition = new LinkedHashMap<String,Set<String>>(); + /* create the property type definition */ + for(Model.Property modelProperty : n.properties.values()){ + Set<String> modifiersTypeDefinition = new LinkedHashSet<String>(); + for(Model.Modifier modifier : modelProperty.modifiers.values()) + modifiersTypeDefinition.add(modifier.type.value); + propertiesTypeDefinition.put(modelProperty.type.value, modifiersTypeDefinition); + } + NodeProperties properties = new NodeProperties(propertiesTypeDefinition); + /* now that properties object is created attach the views on it */ + for(Model.Property modelProperty : n.properties.values()){ + PropertyView propertyView = new PropertyView( + SimpleShapeNode.Position.valueOf(modelProperty.position.value), + modelProperty.shape.value.isEmpty() ? + /* doesn't really matter as position is inside and shape won't be taken into account */ + SimpleShapeNode.ShapeType.Rectangle : + SimpleShapeNode.ShapeType.valueOf(modelProperty.shape.value) + ); + properties.setView(modelProperty.type.value, propertyView); + /* modifier view */ + for(Model.Modifier modelModifier : modelProperty.modifiers.values()){ + boolean bold = false; + boolean italic = false; + boolean underline = false; + String prefix = ""; + String suffix = ""; + for(String value : modelModifier.format.values){ + if(value.equals(resources.getString("modifier.format.bold"))){ + bold = true; + }else if(value.equals(resources.getString("modifier.format.underline"))){ + underline = true; + }else if(value.equals(resources.getString("modifier.format.italic"))){ + italic = true; + }else if(value.equals(resources.getString("modifier.format.prefix"))){ + prefix = modelModifier.affix.values[PREFIX_INDEX]; + }else if(value.equals(resources.getString("modifier.format.suffix"))){ + suffix = modelModifier.affix.values[SUFFIX_INDEX]; + } + } + ModifierView modifierView = new ModifierView(underline,bold,italic,prefix,suffix); + properties.getModifiers(modelProperty.type.value).setView(modelModifier.type.value, modifierView); + } + } + return SimpleShapeNode.getInstance( + SimpleShapeNode.ShapeType.valueOf(n.shape.value), + n.type.value, + properties); + } + + private Edge createDiagramEdge(Model.Edge e){ + /* create the arrow head array out of the string stored in the model */ + ArrowHead[] arrowHeads = new ArrowHead[e.arrowHeads.values.length]; + for(int i=0; i<e.arrowHeads.values.length;i++){ + try { + arrowHeads[i] = ArrowHead.getArrowHeadFromString(e.arrowHeads.values[i]); + } catch (IOException ioe) { + throw new RuntimeException(ioe);// the wizard mustn't allow the user to enter different strings + } + } + return new SimpleShapeEdge( + e.type.value, + LineStyle.valueOf(e.lineStyle.value), + arrowHeads, + e.arrowHeadsDescriptions.values, + Integer.parseInt(e.minNodes.value), + Integer.parseInt(e.maxNodes.value) + ); + } + + private Model createModel(Diagram diagram) { + Model model = new Model(); + if(diagram == null) + return model; + + /* the name isn't copied as the user as to find a new one */ + /* model.diagramName.value = diagram.getName();*/ + + /* nodes */ + for(Node n : diagram.getNodePrototypes()){ + if(!(n instanceof SimpleShapeNode)) + continue; + Model.Node modelNode = createModelNode((SimpleShapeNode)n); + model.nodes.put(modelNode.id,modelNode); + } + /* edges */ + for(Edge e : diagram.getEdgePrototypes()){ + if(!(e instanceof SimpleShapeEdge)) + continue; + Model.Edge modelEdge = createModelEdge((SimpleShapeEdge)e); + model.edges.put(modelEdge.id, modelEdge); + } + return model; + } + + /** + * fills up the model node object with informations from the real diagram node + * @param n + * @return + */ + private Model.Node createModelNode(SimpleShapeNode n){ + Model.Node modelNode = new Model.Node(); + modelNode.type.value = n.getType(); + modelNode.shape.value = n.getShapeType().toString(); + + NodeProperties properties = n.getProperties(); + for(String propertyType : properties.getTypes()){ + Model.Property modelProperty = new Model.Property(); + modelProperty.type.value = propertyType; + /* if the view is not a PropertyView or is null then assign a default value */ + /* it should never happen but it's just to keep it safer and more forward compliant */ + if(! (properties.getView(propertyType) instanceof PropertyView)){ + modelProperty.position.value = SimpleShapeNode.Position.Inside.toString(); + }else{ + PropertyView propertyView = (PropertyView)properties.getView(propertyType); + modelProperty.position.value = propertyView.getPosition().toString(); + modelProperty.shape.value = propertyView.getShapeType().toString(); + } + Modifiers modifiers = properties.getModifiers(propertyType); + for(String modifierType : modifiers.getTypes()){ + Model.Modifier modelModifier = new Model.Modifier(); + modelModifier.type.value = modifierType; + if(modifiers.getView(modifierType) instanceof ModifierView){ + ModifierView modifierView = (ModifierView)modifiers.getView(modifierType); + /* the string array with the modifier values must be created, so the size must be known before */ + int numModifierValues = 0; + if(modifierView.isBold()) + numModifierValues++; + if(modifierView.isItalic()) + numModifierValues++; + if(modifierView.isUnderline()) + numModifierValues++; + if(!modifierView.getPrefix().isEmpty()) + numModifierValues++; + if(!modifierView.getSuffix().isEmpty()) + numModifierValues++; + /* create the string array and fill it up with values */ + modelModifier.format.values = new String[numModifierValues]; + numModifierValues = 0; + if(modifierView.isBold()) + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.bold"); + if(modifierView.isItalic()) + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.italic"); + if(modifierView.isUnderline()) + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.underline"); + if(!modifierView.getPrefix().isEmpty()){ + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.prefix"); + modelModifier.affix.values[PREFIX_INDEX] = modifierView.getPrefix(); + } + + if(!modifierView.getSuffix().isEmpty()){ + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.suffix"); + modelModifier.affix.values[SUFFIX_INDEX] = modifierView.getSuffix(); + } + } + modelProperty.modifiers.put(modelModifier.id, modelModifier); + } + modelNode.properties.put(modelProperty.id, modelProperty); + } + return modelNode; + } + + private Model.Edge createModelEdge(SimpleShapeEdge e){ + Model.Edge modelEdge = new Model.Edge(); + modelEdge.type.value = e.getType(); + modelEdge.lineStyle.value = e.getStyle().toString(); + modelEdge.maxNodes.value = Integer.toString(e.getMaxAttachedNodes()); + modelEdge.minNodes.value = Integer.toString(e.getMinAttachedNodes()); + + /* arrow heads and arrowheads descriptions */ + modelEdge.arrowHeadsDescriptions.values = e.getAvailableEndDescriptions(); + modelEdge.arrowHeads.values = new String[e.getHeads().length]; + for(int i =0; i<e.getHeads().length;i++){ + modelEdge.arrowHeads.values[i] = e.getHeads()[i].toString(); + } + + return modelEdge; + } + + private SpeechWizardDialog dialog; + private Diagram diagram; + private ResourceBundle resources; + private Model model; + /* these are the temporary variables where the data are stored during the wizard * + * when a sub task is completed ( node creation, edge creation, property creation)* + * the data stored in the temporary variables are saved in the model */ + private Model.Node node; + private Model.Property property; + private Model.Modifier modifier; + private Model.Edge edge; + + static int HOME = 0; + static int DIAGRAM_NAME = 1; + static int NODES = 2; + static int NODE_TYPE = 3; + static int NODE_DEL = 4; + static int NODE_EDIT = 5; + static int NODE_SHAPE = 6; + static int NODE_YESNO_PROPERTIES = 7; + static int PROPERTIES = 8; + static int PROPERTY_TYPE = 9; + static int PROPERTY_DEL = 10; + static int PROPERTY_EDIT = 11; + static int PROPERTY_POSITION = 12; + static int PROPERTY_SHAPE = 13; + static int PROPERTY_YESNO_MODIFIER = 14; + static int MODIFIERS = 15; + static int MODIFIER_TYPE = 16; + static int MODIFIER_DEL = 17; + static int MODIFIER_EDIT = 18; + static int MODIFIER_FORMAT = 19; + static int EDGES = 20; + static int EDGE_TYPE = 21; + static int EDGE_DEL = 22; + static int EDGE_EDIT = 23; + static int EDGE_LINE_STYLE = 24; + static int EDGE_MIN_NODES = 25; + static int EDGE_MAX_NODES = 26; + static int EDGE_YESNO_ARROW_HEAD = 27; + static int EDGE_ARROW_HEAD = 28; + static int LAST_PANEL = 29; + + private static int PREFIX_INDEX = 0; + private static int SUFFIX_INDEX = 1; + + /* the abstract class from which the panels for Nodes, edges and Modifiers inherit + * It displays the actions (add,edit,delete,finish) on a comboBox. if elementNames is empty + * it means that no element has been created yet and therefore edit and delete actions are disabled + */ + @SuppressWarnings("serial") + private static class ActionChooserPanel extends SpeechWizardPanel{ + ActionChooserPanel(JWizardComponents wizardComponents,Collection<String> elementNames, String title, String[] options, int[] nexts, int previous, Model.Element temporaryElement){ + super(wizardComponents,title,OWN_SWITCH, previous); + this.options = options; + comboBoxModel = new DefaultComboBoxModel(); + comboBoxModel.addElement(options[0]); + comboBoxModel.addElement(options[3]); + comboBox = new LoopComboBox(comboBoxModel); + comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + layoutComponents(comboBox); + this.elementNames = elementNames; + this.temporaryElement = temporaryElement; + this.nexts = nexts; + } + + @Override + public void update(){ + if(elementNames.isEmpty() && comboBoxModel.getSize() == 4){ + comboBoxModel.removeElement(options[1]); + comboBoxModel.removeElement(options[2]); + }else if(!elementNames.isEmpty() && comboBoxModel.getSize() == 2){ + comboBoxModel.insertElementAt(options[1],1); + comboBoxModel.insertElementAt(options[2],2); + } + super.update(); + } + + @Override + public void next(){ + /* if the selection was add element, then we clear the temporary holder */ + if(comboBox.getSelectedIndex() == 0) + temporaryElement.clear(); + /* jump to the selected next step, works both when it's only add/finish and when it's add/delete/edit/finish */ + for(int i=0; i<options.length; i++) + if(comboBox.getSelectedItem().equals(options[i])){ + switchPanel(nexts[i]); + return; + } + } + + JComboBox comboBox; + Collection<String> elementNames; + DefaultComboBoxModel comboBoxModel; + String[] options; + Model.Element temporaryElement; + int[] nexts; + } + + @SuppressWarnings("serial") + private static class DeletePanel extends SpeechWizardPanel { + DeletePanel(JWizardComponents wizardComponents,String title, int next, int previous, ModelMap<? extends Model.Element> elements){ + super(wizardComponents,title,next, previous); + this.elements = elements; + comboBox = new LoopComboBox(); + comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + layoutComponents(comboBox); + } + + @Override + public void update(){ + String[] options = new String[elements.values().size()]; + options = elements.getNames().toArray(options); + comboBox.setModel(new DefaultComboBoxModel(options)); + super.update(); + } + + /** + * the default behaviour is to delete the selected element + */ + @Override + public void next(){ + Model.Element elementToDelete = null; + for(Model.Element element : elements.values()){ + if(element.type.value.equals(comboBox.getSelectedItem())){ + elementToDelete = element; + break; + } + } + Object o = elements.remove(elementToDelete.id); + assert(o != null); + super.next(); + } + + JComboBox comboBox; + ModelMap<? extends Model.Element> elements; + } + + @SuppressWarnings("serial") + private static class EditPanel extends DeletePanel { + EditPanel(JWizardComponents wizardComponents, + String title, + int next, + int previous, + ModelMap<? extends Model.Element> elements, + Model.Element temporaryHolder){ + super(wizardComponents, title,next, previous,elements); + this.temporaryHolder = temporaryHolder; + this.next = next; + } + + @Override + public void next(){ + Model.Element selected = null; + for(Model.Element e : elements.values()){ + if(e.type.value.equals(comboBox.getSelectedItem())){ + selected = e; + break; + } + } + + Model.copy(selected, temporaryHolder); + switchPanel(next); + } + + int next; + Model.Element temporaryHolder; + } + + @SuppressWarnings("serial") + private class FormatWizardPanel extends SpeechWizardPanel{ + FormatWizardPanel(){ + super(dialog.getWizardComponents(),resources.getString("panel.modifier_format.title"),MODIFIERS,MODIFIER_TYPE); + String values[] = { + resources.getString("modifier.format.bold"), + resources.getString("modifier.format.underline"), + resources.getString("modifier.format.italic"), + resources.getString("modifier.format.prefix"), + resources.getString("modifier.format.suffix"), + }; + + checkBoxes = new JCheckBox[values.length]; + checkBoxPanel = new JPanel(new GridLayout(0, 1)); + for(int i=0; i<values.length;i++){ + String value = values[i]; + checkBoxes[i] = new JCheckBox(value); + checkBoxes[i].getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + checkBoxes[i].getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + checkBoxes[i].addItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + /* prefix and suffix check boxes must have a JText area for the user to enter the String */ + if(i == 3 || i == 4){ + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT,0,0)); + panel.add(checkBoxes[i]); + if(i == 3){ + prefixTextField = new JTextField(); + prefixTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + prefixTextField.getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + prefixTextField.getAccessibleContext().setAccessibleName(checkBoxes[i].getText()); + prefixTextField.setColumns(5); + prefixTextField.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + panel.add(prefixTextField); + }else{ + suffixTextField = new JTextField(); + suffixTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + suffixTextField.getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + suffixTextField.getAccessibleContext().setAccessibleName(checkBoxes[i].getText()); + suffixTextField.setColumns(5); + suffixTextField.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + panel.add(suffixTextField); + } + checkBoxPanel.add(panel); + }else{ + checkBoxPanel.add(checkBoxes[i]); + } + } + JScrollPane scrollPane = new JScrollPane(checkBoxPanel); + scrollPane.setFocusable(false); + layoutComponents(scrollPane); + dialog.getWizardComponents().getFinishButton().setEnabled(true); + } + + /* store the checks into the StrArrayRecord, it doesn't call super.next thus */ + /* sub classes have to implement call the switch panel on their own */ + public void next(){ + int numCheckedBoxes = 0; + for(JCheckBox check : checkBoxes){ + if(check.isSelected()) + numCheckedBoxes++; + } + String[] result = new String[numCheckedBoxes]; + numCheckedBoxes = 0; + for(int i=0; i<checkBoxes.length;i++){ + /* store the text value of the check boxes, if it's the prefix or suffix */ + /* append the text entered by the user in the text areas */ + if(checkBoxes[i].isSelected()){ + String text = checkBoxes[i].getText(); + if(i == 3) + modifier.affix.values[PREFIX_INDEX] = prefixTextField.getText(); + else if(i == 4) + modifier.affix.values[SUFFIX_INDEX] = suffixTextField.getText(); + result[numCheckedBoxes++] = text; + } + } + modifier.format.values = result; + Model.Modifier newModifier = new Model.Modifier(); + Model.copy(modifier,newModifier); + property.modifiers.put(newModifier.id,newModifier); + super.next(); + } + + @Override + public void update(){ + /* set the check boxes and text field according to the modifier.format. so if we are editing an existing * + * modifier we find the old values, else if it's a new modifier we find everything blank */ + if(modifier.format != null){ + prefixTextField.setText(""); + suffixTextField.setText(""); + for(JCheckBox check : checkBoxes){ + /* temporarily remove the speech Item listener in order to avoid bla bla bla not triggered by user */ + check.removeItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + check.setSelected(false); + for(String checkedValue : modifier.format.values){ + if(checkedValue.equals(check.getText())){//for bold,italic,underline + check.setSelected(true); + if(checkedValue.equals(resources.getString("modifier.format.prefix"))){ + prefixTextField.setText(modifier.affix.values[PREFIX_INDEX]); + }else if(checkedValue.equals(resources.getString("modifier.format.suffix"))){ + suffixTextField.setText(modifier.affix.values[SUFFIX_INDEX]); + } + break; + } + } + check.addItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + } + } + super.update(); + } + + @Override + protected Component assignFocus(){ + /* focus on the first item */ + checkBoxes[0].requestFocus(); + return checkBoxes[0]; + } + + JTextField prefixTextField; + JTextField suffixTextField; + JPanel checkBoxPanel; + JCheckBox checkBoxes[]; + } + + @SuppressWarnings("serial") + private class ArrowHeadPanel extends SpeechWizardPanel { + ArrowHeadPanel(){ + super(dialog.getWizardComponents(),resources.getString("panel.edge_arrow_head.title"),EDGES,EDGE_YESNO_ARROW_HEAD); + JPanel panel = new JPanel(new GridBagLayout()); + final JScrollPane scrollPane = new JScrollPane(panel); + scrollPane.setFocusable(false); + GridBagUtilities gridBagUtils = new GridBagUtilities(); + int numArrowHeads = ArrowHead.values().length; + arrowsCheckBoxes = new JCheckBox[numArrowHeads]; + arrowsTextDescriptions = new JTextField[numArrowHeads]; + for(int i=0; i<numArrowHeads; i++){ + /* set up the key bindings for all the check boxes and text fields */ + /* by pressing enter the wizard switches the next panel */ + arrowsCheckBoxes[i] = new JCheckBox(ArrowHead.values()[i].toString()); + arrowsCheckBoxes[i].getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + arrowsCheckBoxes[i].getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + arrowsTextDescriptions[i] = new JTextField(); + arrowsTextDescriptions[i].getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + arrowsTextDescriptions[i].getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + /* add the speech to the check boxes */ + arrowsCheckBoxes[i].addItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + + arrowsTextDescriptions[i].setPreferredSize(new Dimension(TEXTFIELD_SIZE, arrowsTextDescriptions[i].getPreferredSize().height)); + arrowsTextDescriptions[i].getAccessibleContext().setAccessibleName(arrowsCheckBoxes[i].getText()); + arrowsTextDescriptions[i].addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + panel.add(arrowsCheckBoxes[i], gridBagUtils.label()); + panel.add(arrowsTextDescriptions[i],gridBagUtils.field()); + } + layoutComponents(scrollPane); + } + + @Override + public void update(){ + /* restore the values (checkbox + text) currently in edge.arrowHeads into the panel components */ + if(edge.arrowHeads != null){ + for(int i=0; i<arrowsCheckBoxes.length;i++){ + arrowsCheckBoxes[i].setSelected(false); + arrowsTextDescriptions[i].setText(""); + for(int j=0; j< edge.arrowHeads.values.length; j++){ + if(arrowsCheckBoxes[i].getText().equals(edge.arrowHeads.values[j])){ + arrowsCheckBoxes[i].setSelected(true); + arrowsTextDescriptions[i].setText(edge.arrowHeadsDescriptions.values[j]); + break; + } + } + } + } + super.update(); + } + @Override + public void next(){ + /* check that the user has entered a text for all of the selected check boxes */ + int numChecked = 0;//this is to keep count of the checked boxes, used after the check + for(int i=0; i<arrowsCheckBoxes.length;i++){ + JCheckBox checkBox = arrowsCheckBoxes[i]; + if(checkBox.isSelected()){ + numChecked++; + /* there cannot be a checked check box without the related textField filled in */ + if(arrowsTextDescriptions[i].getText().trim().isEmpty()){ + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString("dialog.error.empty_desc"), + checkBox.getText()) + ); + return; + } + } + } + /* copy the label of the checked boxes and the text of the JTextField into the edge fields */ + edge.arrowHeads.values = new String[numChecked]; + edge.arrowHeadsDescriptions.values = new String[numChecked]; + numChecked = 0; + for(int i=0; i<arrowsCheckBoxes.length;i++){ + if(arrowsCheckBoxes[i].isSelected()){ + edge.arrowHeads.values[numChecked] = arrowsCheckBoxes[i].getText(); + edge.arrowHeadsDescriptions.values[numChecked] = arrowsTextDescriptions[i].getText().trim(); + numChecked++; + } + } + /* put the edge (copy of) into the model */ + Model.Edge newEdge = new Model.Edge(); + Model.copy(edge, newEdge); + model.edges.put(newEdge.id,newEdge); + super.next(); + } + + @Override + protected Component assignFocus(){ + /* focus on the first item */ + arrowsCheckBoxes[0].requestFocus(); + return arrowsCheckBoxes[0]; + } + + JCheckBox arrowsCheckBoxes[]; + JTextField arrowsTextDescriptions[]; + final int TEXTFIELD_SIZE = 100; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/AudioResourcesService.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,74 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Set; + +/** + * This class holds the stream of audio files associated to each + * sound event type. The audio file is played out each time the event, associated to + * it, happens. + */ + +public class AudioResourcesService { + /** + * @param type A sound event type + * @return the file name associated to a sound event type + */ + public static InputStream getAudiofile(SoundEvent type){ + if(audioFileNames == null) + audioFileNames = new AudioResourcesService(); + return audioFileNames.nameMap.get(type); + } + + public static Set<SoundEvent> eventTypes(){ + if(audioFileNames == null) + audioFileNames = new AudioResourcesService(); + return audioFileNames.nameMap.keySet(); + } + + private AudioResourcesService(){ + Class<AudioResourcesService> c = AudioResourcesService.class; + nameMap = new LinkedHashMap<SoundEvent, InputStream>(); + nameMap.put(SoundEvent.TREE_NODE_COLLAPSE, c.getResourceAsStream("audio/collapse.mp3")); + nameMap.put(SoundEvent.TREE_NODE_EXPAND, c.getResourceAsStream("audio/expand.mp3")); + nameMap.put(SoundEvent.LIST_BOTTOM_REACHED, c.getResourceAsStream("audio/endoflist.mp3")); + nameMap.put(SoundEvent.LIST_TOP_REACHED, c.getResourceAsStream("audio/endoflist.mp3")); + nameMap.put(SoundEvent.JUMP,c.getResourceAsStream("audio/jump.mp3")); + nameMap.put(SoundEvent.ERROR,c.getResourceAsStream("audio/error.mp3")); + nameMap.put(SoundEvent.OK,c.getResourceAsStream("audio/Ok.mp3")); + nameMap.put(SoundEvent.CANCEL,c.getResourceAsStream("audio/cancel.mp3")); + nameMap.put(SoundEvent.MESSAGE_OK,c.getResourceAsStream("audio/cancel.mp3")); + nameMap.put(SoundEvent.EMPTY,c.getResourceAsStream("audio/cancel.mp3")); + nameMap.put(SoundEvent.EDITING, c.getResourceAsStream("audio/editingMode.mp3")); + nameMap.put(SoundEvent.MAGNET_ON, c.getResourceAsStream("audio/magnetON.mp3")); + nameMap.put(SoundEvent.MAGNET_OFF, c.getResourceAsStream("audio/magnetOFF.mp3")); + nameMap.put(SoundEvent.HOOK_ON,c.getResourceAsStream("audio/hookON.mp3")); + nameMap.put(SoundEvent.HOOK_OFF,c.getResourceAsStream("audio/hookOFF.mp3")); + nameMap.put(SoundEvent.DRAG, c.getResourceAsStream("audio/drag.mp3")); + } + + private LinkedHashMap<SoundEvent, InputStream> nameMap; + private static AudioResourcesService audioFileNames; + public static String FOLDER = "audio/"; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/BeadsSound.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,187 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import net.beadsproject.beads.core.AudioContext; +import net.beadsproject.beads.core.Bead; +import net.beadsproject.beads.core.UGen; +import net.beadsproject.beads.data.Sample; +import net.beadsproject.beads.data.SampleManager; +import net.beadsproject.beads.ugens.Gain; +import net.beadsproject.beads.ugens.SamplePlayer; +import net.beadsproject.beads.ugens.SamplePlayer.LoopType; + +/** + * The Sound interface implementation using the Beads library. + * For more info abou the library see http://www.beadsproject.net/. + */ +class BeadsSound implements Sound { + + public BeadsSound(){ + ac = new AudioContext(); + playerListeners = new HashMap<SoundEvent,PlayerListener>(); + loopPlayers = new HashMap<SoundEvent,UGen>(); + + /* pre load all the sample to avoid future overhead */ + for(SoundEvent key : AudioResourcesService.eventTypes()){ + SampleManager.sample(AudioResourcesService.getAudiofile(key)); + } + ac.start(); + } + + + public void play(InputStream sound, final PlayerListener playerListener) { + if(mute) + return; + SamplePlayer player; + Sample sample = null; + if(sound != null) + sample = SampleManager.sample(sound); + if(sample == null){ + /* we got problems retrieving the sample to play + * call the playerListener method and return */ + if(playerListener != null) + playerListener.playEnded(); + return; + } + player = new SamplePlayer(ac,sample); + player.setKillOnEnd(true); + final Gain g = new Gain(ac,1,MASTER_VOLUME); + g.addInput(player); + + Bead killBill; + if(playerListener != null){ + killBill = new Bead(){ + @Override + protected void messageReceived(Bead message){ + playerListener.playEnded(); + g.kill(); + playingBead = null; + } + }; + }else{ + killBill = new Bead(){ + @Override + protected void messageReceived(Bead message){ + g.kill(); + playingBead = null; + } + }; + } + + player.setKillListener(killBill); + playingBead = g; + ac.out.addInput(g); + } + + public void play(InputStream sound){ + play(sound, null); + } + + @Override + public void play(final SoundEvent evt ){ + if(evt == null){ + InputStream s = null; + play(s); + }else + play(evt,playerListeners.get(evt)); + } + + public void play(SoundEvent evt, PlayerListener playerListener){ + if(evt == null){ + InputStream s = null; + play(s,playerListener); + }else + play(AudioResourcesService.getAudiofile(evt),playerListener); + } + + public void stop(){ + if(mute) + return; + if(playingBead != null){ + playingBead.setKillListener(null); + playingBead.kill(); + } + } + + public void loadSound(InputStream sound){ + SampleManager.sample(sound); + } + + @Override + public void startLoop(SoundEvent action) { + if(mute) + return; + Sample sample = null; + if(action != null){ + InputStream samplePath = AudioResourcesService.getAudiofile(action); + if(samplePath != null) + sample = SampleManager.sample(samplePath); + } + if(sample == null) + return; + SamplePlayer player = new SamplePlayer(ac,sample); + player.setLoopType(LoopType.LOOP_FORWARDS); + Gain g = new Gain(ac,1,MASTER_VOLUME); + g.addInput(player); + ac.out.addInput(g); + loopPlayers.put(action, g); + } + + @Override + public void stopLoop(SoundEvent action) { + UGen g = loopPlayers.get(action); + if(g != null){ + g.kill(); + loopPlayers.remove(action); + } + } + + @Override + public void setPlayerListener(PlayerListener listener, SoundEvent type){ + playerListeners.put(type, listener); + } + + @Override + public void unsetPlayerListener(SoundEvent type){ + playerListeners.remove(type); + } + + @Override + public void setMuted(boolean mute){ + this.mute = mute; + } + + @Override + public void dispose(){ + ac.stop(); + } + + private AudioContext ac; + private Bead playingBead; + private Map<SoundEvent,PlayerListener> playerListeners; + private Map<SoundEvent,UGen> loopPlayers; + private static final float MASTER_VOLUME = 0.35f; + private boolean mute; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/PlayerListener.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,30 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +/** + * {@code PlayerListeners} can be registered to an object implementing the + * {@code Sound} interface. Each time a sound is played registered listeners + * will be triggered just after the sound is over. + * + */ +public interface PlayerListener { + void playEnded(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/Sound.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,59 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +import java.io.InputStream; + +/** + * An object implementing the {@code Sound} interface can be used to play sound + * samples either just once or in a continuous loop. The client class needs to provide + * a reference to an {@code InputStream} to the sample source. + * Furthermore, prebuilt sounds + * associated to the events defined by the {@code SoundEvent} enumeration can be played. + * + */ +public interface Sound { + + public void play(SoundEvent evt); + + public void play(SoundEvent evt, PlayerListener listener); + + public void play(InputStream sound); + + public void play(InputStream sound, PlayerListener listener); + + public void stop(); + + public void setMuted(boolean mute); + + public void loadSound(InputStream sound); + + public void startLoop(SoundEvent evt); + + public void stopLoop(SoundEvent evt); + + public void setPlayerListener(PlayerListener listener, + SoundEvent type); + + public void unsetPlayerListener(SoundEvent type); + + public void dispose(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/SoundEvent.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,45 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +/** + * The events for which the {@code Sound} library provides predefined sounds. + * The sound files sources for each SoundEvent can be retrieved through the {@code AudioResourcesService} + * class. + * + */ +public enum SoundEvent { + ERROR, + TREE_NODE_COLLAPSE, + TREE_NODE_EXPAND, + LIST_TOP_REACHED, + LIST_BOTTOM_REACHED, + JUMP, + OK, + CANCEL, + EMPTY, + EDITING, + MAGNET_ON, + MAGNET_OFF, + HOOK_ON, + HOOK_OFF, + DRAG, + MESSAGE_OK +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/SoundFactory.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,40 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +/** + * The factory class to create {@code Sound} instances. + * + */ +public abstract class SoundFactory { + + public static Sound createInstance(){ + sound = new BeadsSound(); + return sound; + } + + public static Sound getInstance(){ + if(sound == null) + throw new IllegalStateException("createInstance() must be called before any getInstance() call"); + return sound; + } + + private static Sound sound; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/DummyNarrator.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,52 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +/* +* A dummy implementation of the Narrator interface. All its methods are empty, +* so every call will have no effect whatsoever. +*/ +class DummyNarrator implements Narrator { + + @Override + public void init() throws NarratorException {} + + @Override + public void setMuted(boolean muted) {} + + @Override + public void shutUp() {} + + @Override + public void speak(String text) {} + + @Override + public void speakWholeText(String text){} + + @Override + public void dispose() {} + + @Override + public void setRate(int rate) { } + + @Override + public int getRate() {return 0;} + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,47 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +/** + * + * The {@code Narrator} interface provides high level methods to make use of text to speech synthesis. + * + */ +public interface Narrator { + + public void init() throws NarratorException; + + public void setMuted(boolean muted); + + public void setRate(int rate); + + public int getRate(); + + public void shutUp(); + + public void speak(String text); + + public void speakWholeText(String text); + + public void dispose(); + + int MIN_RATE = 0; + int MAX_RATE = 20; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.properties Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,29 @@ +component.button=button +component.text_field=text field +component.text_area=text Area +component.combo_box=selected +component.spinner=number box +component.chech=checked +component.uncheck=unchecked + +char.back_space=back space +char.at=at +char.new_line=new line +char.dot=dot +char.comma=comma +char.colon=colon +char.semi_colon=semi colon +char.lower_than=lower than +char.greater_than=greater than +char.delete=delete +char.sharp=number sign +char.tilde=tilde +char.slash=slash +char.plus=plus +char.dash=dash +char.underscore=underscore +char.space=space + +error.no_speech=Could not create the speech synthesizer + +ccmi_spell=dot c c m i
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/NarratorException.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,38 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +import java.util.ResourceBundle; + +/** + * A {@code NarratorException} is thrown when a text to speech synthesizer cannot be instantiated + * for any reason. + * + */ +@SuppressWarnings("serial") +public class NarratorException extends Exception { + public NarratorException(String msg){ + super(msg); + } + + public NarratorException(){ + super(ResourceBundle.getBundle(Narrator.class.getName()).getString("error.no_speech")); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/NarratorFactory.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,56 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +import java.util.logging.Logger; + +import uk.ac.qmul.eecs.ccmi.utils.OsDetector; + +/** + * The factory class to create {@code Narrator} instances. + * + */ +public abstract class NarratorFactory { + + public static Narrator createInstance(){ + if(singleNarrator == null){ + if(OsDetector.isWindows()){ + try{ + singleNarrator = new NativeNarrator(); + singleNarrator.init(); + }catch(NarratorException ne){ + singleNarrator = new DummyNarrator(); + Logger.getLogger("general").warning("Could not enable text to speech synthesis"); + } + }else{ + singleNarrator = new DummyNarrator(); + } + } + return singleNarrator; + } + + public static Narrator getInstance(){ + if(singleNarrator == null) + throw new IllegalStateException("createInstance() must be called before any getInstance() call"); + return singleNarrator; + } + + private static Narrator singleNarrator; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/NativeNarrator.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,185 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +import java.net.URL; +import java.util.ResourceBundle; +import java.util.concurrent.LinkedBlockingQueue; + +import uk.ac.qmul.eecs.ccmi.utils.NativeLibFileWriter; +import uk.ac.qmul.eecs.ccmi.utils.OsDetector; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/* + * Implementation of the Narrator interface using the Windows system text to speech + * synthesizer. + */ +class NativeNarrator implements Narrator { + + static { + nativeLibraryNotFound = true; + if(OsDetector.isWindows()){ + URL url = NativeNarrator.class.getResource("WinNarrator.dll"); + if(url != null){ + NativeLibFileWriter fileWriter = new NativeLibFileWriter(url); + fileWriter.writeToDisk("CCmIWinNarrator.dll"); + String path = fileWriter.getFilePath(); + if(path != null) + try{ + System.load( path ); + nativeLibraryNotFound = false; + }catch(UnsatisfiedLinkError e){ + /* do nothing: nativeLibraryNotFound won't be set to false */ + /* which will trigger a NarratorException */ + } + } + } + } + + public NativeNarrator(){ + resources = ResourceBundle.getBundle(Narrator.class.getName()); + } + + @Override + public void init() throws NarratorException { + if(nativeLibraryNotFound) + throw new NarratorException(); + + muted = false; + queue = new LinkedBlockingQueue<QueueEntry>(); + executor = new Executor(); + boolean success = _init(); + if(!success) + throw new NarratorException(); + rate = Integer.parseInt(PreferencesService.getInstance().get("speech_rate", DEFAULT_RATE_VALUE)); + _setRate(rate); + executor.start(); + } + + @Override + public void setMuted(boolean muted) { + this.muted = muted; + } + + @Override + public void setRate(int rate){ + if(rate < MIN_RATE || rate > MAX_RATE) + throw new IllegalArgumentException("Rate value must be between 0 and 20"); + _setRate(rate); + this.rate = rate; + PreferencesService.getInstance().put("speech_rate", Integer.toString(rate)); + } + + @Override + public int getRate(){ + return rate; + } + + @Override + public void shutUp() { + _shutUp(); + } + + @Override + public void speak(String text) { + if(muted) + return; + if(" ".equals(text)) + text = resources.getString("char.space"); + else if("\n".equals(text)) + text = resources.getString("char.new_line"); + else if(text.contains(".ccmi")) + text = text.replaceAll(".ccmi", " "+resources.getString("ccmi_spell")); + queue.add(new QueueEntry(text,false)); + } + + @Override + public void speakWholeText(String text) { + if(muted) + return; + queue.add(new QueueEntry(text,true)); + } + + @Override + public void dispose() { + executor.mustSayGoodbye = true; + executor.interrupt(); + } + + /* native routines used by the Executor thread */ + private native boolean _init(); + + private native void _dispose(); + + private native void _speak(String text); + + private native void _speakWholeText(String text); + + private native void _setRate(int rate); + + private native void _shutUp(); + + private boolean muted; + private int rate; + private LinkedBlockingQueue<QueueEntry> queue; + private Executor executor; + private ResourceBundle resources; + private static String DEFAULT_RATE_VALUE = "13"; + private static boolean nativeLibraryNotFound; + + private class Executor extends Thread{ + private Executor(){ + super("Narrator Thread"); + mustSayGoodbye = false; + } + + @Override + public void run(){ + QueueEntry entry; + while(!mustSayGoodbye){ + try { + entry = queue.take(); + } catch (InterruptedException e) { + continue; /* start over the while cycle */ + } + if(!entry.speakToEnd && queue.peek() != null) + continue;/* the user submitted another text to be spoken out and this can be overwritten */ + if(entry.speakToEnd){ + _speakWholeText(entry.text); + }else{ + _speak(entry.text); + } + } + _dispose(); + } + + private volatile boolean mustSayGoodbye; + } + + private static class QueueEntry { + QueueEntry(String text, boolean speakToEnd){ + this.text = text; + this.speakToEnd = speakToEnd; + } + String text; + boolean speakToEnd; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/SpeechUtilities.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,374 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +import java.awt.AWTKeyStroke; +import java.awt.Component; +import java.awt.Container; +import java.awt.FocusTraversalPolicy; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JSpinner; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; + +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * A class providing static utilities methods concerning the text to speech synthesis. + * + */ +public abstract class SpeechUtilities { + /* this class is of static use only */ + private SpeechUtilities(){} + + public static String getComponentSpeech(Component c){ + StringBuilder b = new StringBuilder(); + if(c.getAccessibleContext().getAccessibleName() != null) + b.append(c.getAccessibleContext().getAccessibleName()); + if(c instanceof JButton) + b.append(' ').append(resources.getString("component.button")); + else if(c instanceof JTextField){ + b.append(' ').append(resources.getString("component.text_field")); + b.append(((JTextField)c).getText()); + }else if(c instanceof JTextArea){ + b.append(' ').append(resources.getString("component.text_area")); + b.append(((JTextArea)c).getText()); + }else if(c instanceof JComboBox){ + b.append(((JComboBox)c).getSelectedItem().toString()); + b.append(' ').append(resources.getString("component.combo_box")); + }else if(c instanceof JCheckBox){ + b.append(' ').append(((JCheckBox)c).isSelected() ? resources.getString("component.chech") : resources.getString("component.uncheck")); + }else if(c instanceof JSpinner){ + b.append(' ').append(resources.getString("component.spinner")); + b.append(((JSpinner)c).getValue()); + }else{ + b.append(' ').append(c.getAccessibleContext().getAccessibleRole()); + } + return b.toString(); + } + + @SuppressWarnings("serial") + public static void changeTabListener(JComponent component, final Container container){ + /* remove the default tab traversal key from all the containers */ + disableTraversalKey(component); + /* get the look and feel default keys for moving the focus on (usually = TAB) */ + for(AWTKeyStroke keyStroke : KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)) + component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(keyStroke.getKeyCode(),keyStroke.getModifiers()),"tab"); + + /* add action to the moving focus keys: reproduce focus system and add speech to it */ + component.getActionMap().put("tab", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FocusTraversalPolicy policy = KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalPolicy(); + Component next = policy.getComponentAfter(container, KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); + next.requestFocusInWindow(); + NarratorFactory.getInstance().speak(SpeechUtilities.getComponentSpeech(next)); + } + }); + + for(AWTKeyStroke keyStroke : KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)) + component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(keyStroke.getKeyCode(),keyStroke.getModifiers()),"back_tab"); + + component.getActionMap().put("back_tab", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FocusTraversalPolicy policy = KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalPolicy(); + Component previous = policy.getComponentBefore(container, KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); + previous.requestFocusInWindow(); + NarratorFactory.getInstance().speak(SpeechUtilities.getComponentSpeech(previous)); + } + }); + } + + private static void disableTraversalKey(Container container){ + for(final Component c : container.getComponents()){ + if(c instanceof Container){ + c.setFocusTraversalKeysEnabled(false); + disableTraversalKey((Container)c); + } + } + } + + public static KeyListener getSpeechKeyListener(boolean editableComponent){ + if(!editableComponent) + return new SpeechKeyListener(false); + return speechKeyListener; + } + + public static ItemListener getSpeechComboBoxItemListener(){ + return comboBoxItemListener; + } + + public static ItemListener getCheckBoxSpeechItemListener(){ + return checkBoxItemListener; + } + + public static FocusListener getFocusSpeechListener(){ + return focusListener; + } + + public static Action getShutUpAction(){ + return shutUpAction; + } + + /* + * this class manages the speech feedback when moving around a text component + * with the up, down, left and right arrows + */ + private static class SpeechKeyListener extends KeyAdapter{ + boolean isTab; + boolean isBeginning; + boolean isFirstLine; + boolean isLastLine; + boolean editableComponent; + + SpeechKeyListener(boolean editablecomponent){ + this.editableComponent = editablecomponent; + } + + @Override + public void keyTyped(KeyEvent evt){ + /* this will manage digit or letter characters */ + if(!isTab && !evt.isControlDown() && editableComponent){ + if(Character.isLetterOrDigit(evt.getKeyChar())){ + NarratorFactory.getInstance().speak(String.valueOf(evt.getKeyChar())); + }else{ + /* this will manage special characters with a letter representation */ + switch(evt.getKeyChar()){ + case '\n' : + if(!(evt.getSource() instanceof JTextField)) + NarratorFactory.getInstance().speak(resources.getString("char.new_line")); + break; + case ' ' : + NarratorFactory.getInstance().speak(resources.getString("char.space")); + break; + case '@' : + NarratorFactory.getInstance().speak(resources.getString("char.at")); + break; + case '.' : + NarratorFactory.getInstance().speak(resources.getString("char.dot")); + break; + case ',' : + NarratorFactory.getInstance().speak(resources.getString("char.comma")); + break; + case ';' : + NarratorFactory.getInstance().speak(resources.getString("char.semi_colon")); + break; + case ':' : + NarratorFactory.getInstance().speak(resources.getString("char.colon")); + break; + case '<' : + NarratorFactory.getInstance().speak(resources.getString("char.lower_than")); + break; + case '>' : + NarratorFactory.getInstance().speak(resources.getString("char.greater_than")); + break; + case '#' : + NarratorFactory.getInstance().speak(resources.getString("char.sharp")); + break; + case '~' : + NarratorFactory.getInstance().speak(resources.getString("char.tilde")); + break; + case '+' : + NarratorFactory.getInstance().speak(resources.getString("char.plus")); + break; + case '-' : + NarratorFactory.getInstance().speak(resources.getString("char.dash")); + break; + case '_' : + NarratorFactory.getInstance().speak(resources.getString("char.underscore")); + break; + case '/' : + NarratorFactory.getInstance().speak(resources.getString("char.slash")); + break; + } + } + } + isTab = false; + } + + /* manages all the non digit or letter characters */ + @Override + public void keyPressed(KeyEvent e){ + int caretPos = ((JTextComponent)e.getSource()).getCaretPosition(); + String text = ((JTextComponent)e.getSource()).getText(); + + if (e.getKeyCode() == KeyEvent.VK_TAB){ + isTab = true; + } + if(caretPos == 0) + isBeginning = true; + else + isBeginning = false; + + isFirstLine = true; + for(int i=0; i<caretPos;i++){ + if(text.charAt(i) == '\n'){ + isFirstLine = false; + break; + } + } + + if(text.indexOf('\n', caretPos) == -1) + isLastLine = true; + else + isLastLine = false; + } + + @Override + public void keyReleased(KeyEvent evt){ + JTextComponent textComponent = (JTextComponent)evt.getSource(); + String text; + int begin,end,caretPos; + + switch(evt.getKeyCode()){ + case KeyEvent.VK_BACK_SPACE: + NarratorFactory.getInstance().speak(resources.getString("char.back_space")); + break; + case KeyEvent.VK_DELETE : + NarratorFactory.getInstance().speak(resources.getString("char.delete")); + break; + case KeyEvent.VK_LEFT : + case KeyEvent.VK_RIGHT : + try { + if(evt.getKeyCode() == KeyEvent.VK_LEFT){ //left + if(textComponent.getCaretPosition() == 0 && isBeginning){ + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + }else{ // right + if(textComponent.getCaretPosition() == textComponent.getText().length()){ + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + } + NarratorFactory.getInstance().speak(textComponent.getText(textComponent.getCaretPosition(),1)); + } catch (BadLocationException e1) { + e1.printStackTrace(); + } + break; + case KeyEvent.VK_UP : + /* when moving up and down, the line we land on is spoken out (the whole line). If the border + * (top/bottom most line) is reached then the error sound is played. on a JTextField this + * is the default behaviour with up and down keys as we only have one line + */ + + if(isFirstLine){//we're on the first line and cannot go any upper + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + + text = textComponent.getText(); + caretPos = textComponent.getCaretPosition(); + + /* look for the beginning of the row the cursor is */ + begin = 0; + for(int i=0; i<caretPos;i++){ + if(text.charAt(i) == '\n') + begin = i+1; + } + + /* now the end */ + end = text.indexOf('\n', caretPos); + + if(end == begin)//in case it's an empty line + end++; + NarratorFactory.getInstance().speak(text.substring(begin, end)); + break; + case KeyEvent.VK_DOWN : + if(isLastLine){ //no new line we either have one line only or sit on the last one + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + + text = textComponent.getText(); + caretPos = textComponent.getCaretPosition(); + + begin = 0; + for(int i=0;i<caretPos;i++){ + if(text.charAt(i) == '\n') + begin = i+1; + } + begin = Math.min(begin, text.length()-1); + + end = text.indexOf('\n', begin); + if(end == -1) // the line we're looking for is the last one + end = text.length()-1; + + if(end == begin) // in case it's an empty line + end++; + NarratorFactory.getInstance().speak(text.substring(begin, end)); + break; + } + } + } + + private static final ResourceBundle resources = ResourceBundle.getBundle(Narrator.class.getName()); + private static final SpeechKeyListener speechKeyListener = new SpeechKeyListener(true); + private static final ItemListener comboBoxItemListener = new ItemListener(){ + @Override + public void itemStateChanged(ItemEvent evt) { + if(evt.getStateChange() == ItemEvent.SELECTED) + NarratorFactory.getInstance().speak(evt.getItem().toString()); + } + }; + + private static final ItemListener checkBoxItemListener = new ItemListener(){ + @Override + public void itemStateChanged(ItemEvent evt) { + NarratorFactory.getInstance().speak(getComponentSpeech(((JCheckBox)evt.getItemSelectable()))); + } + }; + + private static final FocusListener focusListener = new FocusAdapter(){ + public void focusGained(FocusEvent evt){ + NarratorFactory.getInstance().speak(getComponentSpeech(evt.getComponent())); + } + }; + + @SuppressWarnings("serial") + private static final Action shutUpAction = new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent e) { + NarratorFactory.getInstance().shutUp(); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/CCmIUncaughtExceptionHandler.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,73 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The UncaughtExceptionHandler for the CCmI Editor. It logs the occurred exception stack trace + * on a file (errorN.log, where N is an integer number automatically assigned to avoid + * collision with other files of the same type) which is created in the same directory where + * the program is run. The exception stack trace will be in the format defined by the {@code XMLFormatter} class + * of the java.utli.logging package. + * + * @see java.util.logging.XMLFormatter + */ +public class CCmIUncaughtExceptionHandler implements UncaughtExceptionHandler { + + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + try{ + Logger logger = Logger.getLogger("uncaught_exception"); + logger.setLevel(Level.SEVERE); + FileHandler fileHandler = null; + try { + fileHandler = new FileHandler("error%u.log",true); + } catch (IOException e) { + System.err.println(throwable.toString()); + System.err.println(); + System.err.println("Could not use error log file"); + e.printStackTrace(); + return; + } + fileHandler.setLevel(Level.SEVERE); + logger.addHandler(fileHandler); + StringBuilder builder = new StringBuilder(throwable.toString()); + builder.append('\n'); + final Writer result = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(result); + throwable.printStackTrace(printWriter); + builder.append(result.toString()); + logger.severe(builder.toString()); + fileHandler.close(); + throwable.printStackTrace(); + }catch(Exception exception){ + exception.printStackTrace(); + System.exit(-1); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/CharEscaper.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,96 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +/** + * A utility class providing static methods to escape one or more characters. + * + */ +public class CharEscaper { + /** + * Replaces the new line character with a '|' in the {@code String} passed as argument. + * The original {@code String} can be restored by passing the returned {@code String} to + * {@link #restoreNewline(String)}. Existing '|' characters will be escaped in order not to miss + * them in the restore process. + * @param s the {@String} to remove new line characters from + * @return a {@String} where all new line characters have been replaced by '|' + */ + public static String replaceNewline(String s){ + String result = s.replace("|", "'|"); + return result.replace('\n', '|'); + } + + /** + * Restores a {@code String} whose new line characters have been previously replaced by + * {@link #replaceNewline(String)}, to the original form. + * @param s + * @return + */ + public static String restoreNewline(String s){ + String result = s.replaceAll("([^'])\\|","$1\n"); + return result.replace("'|", "|"); + } + + /** + * Escapes a set of character with another character. + * @param s The {@code String} whose characters must be escaped. + * @param charsToEscape The set of characters to escape + * @param escapeChar The escape character + * @return a new {@code String} where characters in {@code charsToEscape} have been escaped + * by {@code escapeChar}. + */ + public static String escapeCharSequence(String s, CharSequence charsToEscape, char escapeChar ){ + String result = s; + for(int i=0;i< charsToEscape.length();i++){ + char c = charsToEscape.charAt(i); + if(c == escapeChar) + throw new IllegalArgumentException("escape character cannot be in chars to escape sequence"); + for(int j=0;j<i;j++) + if(charsToEscape.charAt(j) == c) + throw new IllegalArgumentException("chars to escape sequence can only have unique characters"); + result = result.replace(""+c, ""+escapeChar+c); + } + return result; + } + + /** + * Removes an escape character preceding a set of character. + * @param s The {@code String} containing the characters to un-escape + * @param charsToUnescape The set of characters to the which must be un-escaped + * @param escapeChar The escape character + * @return A {@code String} where the escape character preceding the characters + * in charsToUnescape have been removed. + */ + public static String unescapeCharSequence(String s, CharSequence charsToUnescape, char escapeChar ){ + String result = s; + for(int i=0;i< charsToUnescape.length();i++){ + char c = charsToUnescape.charAt(i); + if(c == escapeChar) + throw new IllegalArgumentException("escape character cannot be in chars to escape sequence"); + for(int j=0;j<i;j++) + if(charsToUnescape.charAt(j) == c) + throw new IllegalArgumentException("chars to escape sequence can only have unique characters"); + result = result.replace(""+escapeChar+c,""+c); + } + return result; + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/ExceptionHandler.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,24 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +public interface ExceptionHandler { + void handleException(Exception e); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/GridBagUtilities.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,87 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.awt.GridBagConstraints; + +/** + * + * A Utility class providing static method to quickly arrange components, laid out by + * a GridBagLayout, in the following way: one component per row and + * either taking the whole column or just the right part of it, if preceded by a label. + * + * + */ +public class GridBagUtilities { + public GridBagUtilities(){ + labelPad = DEFAULT_LABEL_PAD; + row = 0; + } + + public GridBagConstraints label(int pad){ + GridBagConstraints c ; + + c = new GridBagConstraints(); + c.anchor = GridBagConstraints.WEST; + c.gridx = 0; + c.gridy = row; + c.insets = new java.awt.Insets(PAD,PAD,PAD,pad); + + return c; + } + + public GridBagConstraints label(){ + return label(labelPad); + } + + public void setLabelPad(int labelPad){ + this.labelPad = labelPad; + } + + public GridBagConstraints field(){ + GridBagConstraints c; + + c = new GridBagConstraints(); + c.anchor = GridBagConstraints.CENTER; + c.gridx = 1; + c.gridy = row++; + c.insets = new java.awt.Insets(PAD,PAD,PAD,PAD); + c.fill = GridBagConstraints.HORIZONTAL; + + return c; + } + + public GridBagConstraints all(){ + GridBagConstraints c; + + c = new GridBagConstraints(); + c.gridy = row++; + c.anchor = GridBagConstraints.CENTER; + c.gridwidth = GridBagConstraints.REMAINDER; + c.fill = GridBagConstraints.HORIZONTAL; + c.insets = new java.awt.Insets(PAD,PAD,PAD,PAD); + return c; + } + + private int labelPad; + private int row; + public static final int DEFAULT_LABEL_PAD = 50; + public static final int PAD = 2; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/InteractionLog.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,112 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * A logger class using the {@code java.util.logging} package to log all the user's + * relevant actions. + */ +public class InteractionLog { + public static void enable(String logFileDir) throws IOException{ + logger.setLevel(Level.FINE); + logger.setUseParentHandlers(false); + if(fileHandler == null){ + fileHandler = new FileHandler(logFileDir+System.getProperty("file.separator")+"interaction%u.log",true); + fileHandler.setFormatter(new CCmILogFormatter()); + logger.addHandler(fileHandler); + } + + /* also print the log on the console */ + java.util.logging.ConsoleHandler ch = new java.util.logging.ConsoleHandler(); + ch.setLevel(Level.ALL); + ch.setFormatter(new CCmILogFormatter()); + logger.addHandler(ch); + } + + public static void disable(){ + logger.setLevel(Level.OFF); + } + + public static CCmILogFormatter newFormatter(){ + return new CCmILogFormatter(); + } + + public static void log(String source, String action, String args){ + StringBuilder builder = new StringBuilder(source); + builder.append(SEPARATOR) + .append(action) + .append(SEPARATOR) + .append(args); + + logger.fine(builder.toString()); + } + + public static void log(String msg){ + logger.config(msg); + } + + public static class CCmILogFormatter extends Formatter{ + + private CCmILogFormatter(){ + super(); + } + + @Override + public String format(LogRecord record) { + StringBuilder builder = new StringBuilder(); + if(record.getLevel() == Level.CONFIG){ + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss"); + builder.append("--- ") + .append(dateFormat.format(new Date(record.getMillis()))) + .append(" - ") + .append(record.getMessage()) + .append(" ---") + .append(NEW_LINE); + }else if(record.getLevel() == Level.FINE){ + SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss.SSS"); + builder.append(dateFormat.format(new Date(record.getMillis()))) + .append(SEPARATOR) + .append(record.getMessage()) + .append(NEW_LINE); + } + + return builder.toString(); + } + } + + public static void dispose(){ + if(fileHandler != null) + fileHandler.close(); + } + + private static Logger logger = Logger.getLogger("interaction"); + private static FileHandler fileHandler; + private static char SEPARATOR = ','; + private final static String NEW_LINE = System.getProperty("line.separator"); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/NativeLibFileWriter.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,87 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** + * This class is used to extract a native library (e.g. .dll file in windows) from within a jar + * to the local file system, in order to allow the virtual machine to load it. + * + */ +public class NativeLibFileWriter { + + /** + * Creates an instance of the the class linked to a native library file. + * @param resource the URL of the native library file. The URL can be obtained by + * @see Class#getResource(String), therefore can be called from a class within a jar file + * which needs to access a static library. + */ + public NativeLibFileWriter(URL resource){ + this.resource = resource; + } + + /** + * Writes the file in a directory returned by {@link PreferencesService#get(String, String)}} if defined, or + * the System default temporary directory otherwise. + * The path to the file can be retrieved by @see {@link #getFilePath()} and then passed as argument + * to {@link System#load(String)} + * + * @param prefix a prefix the temporary native library file will have in the temporary directory + */ + public void writeToDisk(String fileName){ + InputStream in = null; + FileOutputStream out = null; + File lib = new File(PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")),fileName); + if(lib.exists()){ //if dll already exists. no job needs to be done. + path = lib.getAbsolutePath(); + return; + } + try{ + in = resource.openStream(); + out = new FileOutputStream(lib); + int byteRead; + byte[] b = new byte[1024]; + while((byteRead = in.read(b)) > 0){ + out.write(b, 0, byteRead); + } + path = lib.getAbsolutePath(); + }catch(IOException ioe){ + path = null; + }finally{ + if(in != null) + try{in.close();}catch(IOException ioe){} + if(out != null) + try{out.close();}catch(IOException ioe){} + } + + } + + public String getFilePath(){ + return path; + } + + private URL resource; + private String path; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/OsDetector.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,42 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +/** + * + * Provides static methods to probe which Operating System we're running on + * + */ +public class OsDetector { + + public static boolean isWindows(){ + return(OS.indexOf( "win" ) >= 0); + } + + public static boolean isMac(){ + return(OS.indexOf( "mac" ) >= 0); + } + + public static boolean isUnix(){ + return(OS.indexOf( "nix") >=0 || OS.indexOf( "nux") >=0); + } + + static final String OS = System.getProperty("os.name").toLowerCase(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/Pair.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,69 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +/** + * + * A Pair of objects. + * + * @param <T1> The first item type + * @param <T2> The second item type + */ +public class Pair<T1,T2> { + public Pair(T1 first, T2 second){ + this.first = first; + this.second = second; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((first == null) ? 0 : first.hashCode()); + result = prime * result + ((second == null) ? 0 : second.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + @SuppressWarnings("rawtypes") + Pair other = (Pair) obj; + if (first == null) { + if (other.first != null) + return false; + } else if (!first.equals(other.first)) + return false; + if (second == null) { + if (other.second != null) + return false; + } else if (!second.equals(other.second)) + return false; + return true; + } + + public T1 first; + public T2 second; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/PreferencesService.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,91 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.util.prefs.Preferences; + +/** + * A service for storing and loading user preferences. + */ +public abstract class PreferencesService +{ + /** + * Gets an instance of the service, suitable for the package of the given class. + * @return an instance of the service + */ + public static PreferencesService getInstance(){ + if (service != null) return service; + try{ + service = new DefaultPreferencesService(); + return service; + } + catch (SecurityException exception){ + // that happens when we run under Web Start + } + + return new NullPreferencesService(); + } + + /** + * Gets a previously stored string from the service. + * @param key the key of the string + * @param defval the value to return if no matching value was found + * @return the value stored with the given key, or defval if none was found + */ + public abstract String get(String key, String defval); + /** + * Saves a key/value pair for later retrieval. + * @param key the key of the string to be stored + * @param value the value to to be stored + */ + public abstract void put(String key, String value); + + private static PreferencesService service; +} + +/** + * The default preferences service that uses the java.util.prefs API. + */ +class DefaultPreferencesService extends PreferencesService{ + /** + * Gets an instance of the service, suitable for the package of the given class. + * @param appClass the main application class (only the package name is used as the path to + * app-specific preferences storage) + * @return an instance of the service + */ + public DefaultPreferencesService() + { + prefs = Preferences.userNodeForPackage(this.getClass()); + } + + public String get(String key, String defval) { return prefs.get(key, defval); } + public void put(String key, String defval) { prefs.put(key, defval); } + + private Preferences prefs; +} + +/** + * The null preferences service that is returned when we are an applet. + */ +class NullPreferencesService extends PreferencesService { + public String get(String key, String defval) { return defval; } + public void put(String key, String defval) { } +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/Validator.java Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,33 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Validator { + + public static boolean validateIPAddr(String addr){ + Matcher m = ip.matcher(addr); + return m.matches(); + } + + private static Pattern ip = Pattern.compile("\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b"); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/license.txt Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/WinNarrator/ReadMe.txt Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,33 @@ +======================================================================== + CONSOLE APPLICATION : WinNarrator Project Overview +======================================================================== + +AppWizard has created this WinNarrator application for you. + +This file contains a summary of what you will find in each of the files that +make up your WinNarrator application. + + +WinNarrator.vcproj + This is the main project file for VC++ projects generated using an Application Wizard. + It contains information about the version of Visual C++ that generated the file, and + information about the platforms, configurations, and project features selected with the + Application Wizard. + +WinNarrator.cpp + This is the main application source file. + +///////////////////////////////////////////////////////////////////////////// +Other standard files: + +StdAfx.h, StdAfx.cpp + These files are used to build a precompiled header (PCH) file + named WinNarrator.pch and a precompiled types file named StdAfx.obj. + +///////////////////////////////////////////////////////////////////////////// +Other notes: + +AppWizard uses "TODO:" comments to indicate parts of the source code you +should add to or customize. + +/////////////////////////////////////////////////////////////////////////////
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/WinNarrator/WinNarrator.cpp Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,11 @@ +// WinNarrator.cpp : Defines the entry point for the console application. +// + +#include "stdafx.h" + + +int _tmain(int argc, _TCHAR* argv[]) +{ + return 0; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/WinNarrator/WinNarrator.vcproj Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,231 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<VisualStudioProject + ProjectType="Visual C++" + Version="9.00" + Name="WinNarrator" + ProjectGUID="{21E0931A-4AE4-4665-801B-DE5D38DF9588}" + RootNamespace="WinNarrator" + Keyword="Win32Proj" + TargetFrameworkVersion="196613" + > + <Platforms> + <Platform + Name="Win32" + /> + </Platforms> + <ToolFiles> + </ToolFiles> + <Configurations> + <Configuration + Name="Debug|Win32" + OutputDirectory="..\workspace\ccmi" + IntermediateDirectory="$(ConfigurationName)" + ConfigurationType="2" + CharacterSet="1" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + /> + <Tool + Name="VCCLCompilerTool" + Optimization="0" + AdditionalIncludeDirectories=""C:\Program Files\Java\jdk1.6.0_25\include";"C:\Program Files\Java\jdk1.6.0_25\include\win32"" + PreprocessorDefinitions="WIN32;_DEBUG;_CONSOLE" + MinimalRebuild="true" + BasicRuntimeChecks="3" + RuntimeLibrary="3" + UsePrecompiledHeader="2" + WarningLevel="3" + DebugInformationFormat="4" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLinkerTool" + LinkIncremental="2" + GenerateDebugInformation="true" + SubSystem="1" + TargetMachine="1" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCManifestTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCAppVerifierTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + <Configuration + Name="Release|Win32" + OutputDirectory="$(SolutionDir)$(ConfigurationName)" + IntermediateDirectory="$(ConfigurationName)" + ConfigurationType="2" + CharacterSet="1" + WholeProgramOptimization="1" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + /> + <Tool + Name="VCCLCompilerTool" + Optimization="2" + EnableIntrinsicFunctions="true" + AdditionalIncludeDirectories=""C:\Program Files\Java\jdk1.6.0_25\include";"C:\Program Files\Java\jdk1.6.0_25\include\win32"" + PreprocessorDefinitions="WIN32;NDEBUG;_CONSOLE" + RuntimeLibrary="2" + EnableFunctionLevelLinking="true" + UsePrecompiledHeader="2" + WarningLevel="3" + DebugInformationFormat="3" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLinkerTool" + LinkIncremental="1" + GenerateDebugInformation="true" + SubSystem="1" + OptimizeReferences="2" + EnableCOMDATFolding="2" + TargetMachine="1" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCManifestTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCAppVerifierTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + </Configurations> + <References> + </References> + <Files> + <Filter + Name="Source Files" + Filter="cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx" + UniqueIdentifier="{4FC737F1-C7A5-4376-A066-2A32D752A2FF}" + > + <File + RelativePath=".\stdafx.cpp" + > + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + UsePrecompiledHeader="1" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + UsePrecompiledHeader="1" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\uk_ac_qmul_eecs_ccmi_speech_NativeWinNarrator.cpp" + > + </File> + </Filter> + <Filter + Name="Header Files" + Filter="h;hpp;hxx;hm;inl;inc;xsd" + UniqueIdentifier="{93995380-89BD-4b04-88EB-625FBE52EBFB}" + > + <File + RelativePath=".\stdafx.h" + > + </File> + <File + RelativePath=".\targetver.h" + > + </File> + <File + RelativePath=".\uk_ac_qmul_eecs_ccmi_speech_NativeWinNarrator.h" + > + </File> + </Filter> + <Filter + Name="Resource Files" + Filter="rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav" + UniqueIdentifier="{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}" + > + </Filter> + <File + RelativePath=".\ReadMe.txt" + > + </File> + </Files> + <Globals> + </Globals> +</VisualStudioProject>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/WinNarrator/stdafx.cpp Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,28 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + + +// stdafx.cpp : source file that includes just the standard includes +// WinNarrator.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/WinNarrator/stdafx.h Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,34 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" + +#include <stdio.h> +#include <tchar.h> + + + +// TODO: reference additional headers your program requires here
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/WinNarrator/targetver.h Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,32 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +// The following macros define the minimum required platform. The minimum required platform +// is the earliest version of Windows, Internet Explorer etc. that has the necessary features to run +// your application. The macros work by enabling all features available on platform versions up to and +// including the version specified. + +// Modify the following defines if you have to target a platform prior to the ones specified below. +// Refer to MSDN for the latest info on corresponding values for different platforms. +#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows Vista. +#define _WIN32_WINNT 0x0600 // Change this to the appropriate value to target other versions of Windows. +#endif +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/WinNarrator/uk_ac_qmul_eecs_ccmi_speech_NativeWinNarrator.cpp Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,88 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" +#include <sapi.h> +#include "uk_ac_qmul_eecs_ccmi_speech_NativeWinNarrator.h" +#include <iostream> + +void __speak(JNIEnv *env, jstring text, DWORD dwFlags); + +ISpVoice * pVoice; + +JNIEXPORT jboolean JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1init +(JNIEnv *env, jobject obj){ + pVoice = NULL; + if (FAILED(::CoInitialize(NULL))) + return JNI_FALSE; + + HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&pVoice); + + if( SUCCEEDED( hr ) ) + return JNI_TRUE; + else + return JNI_FALSE; +} + +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1dispose +(JNIEnv *env, jobject obj){ + pVoice->Release(); + pVoice = NULL; + ::CoUninitialize(); +} + +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1speak +(JNIEnv *env, jobject obj, jstring text){ + __speak(env,text,SPF_ASYNC | SPF_PURGEBEFORESPEAK | SPF_IS_NOT_XML); +} + +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1speakWholeText +(JNIEnv *env, jobject obj, jstring text){ + __speak(env,text,SPF_DEFAULT | SPF_PURGEBEFORESPEAK | SPF_IS_NOT_XML); +} + +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1setRate +(JNIEnv *env, jobject obj, jint rate){ + pVoice->SetRate(rate-10); +} + +void __speak(JNIEnv *env, jstring text, DWORD dwFlags){ + int len = env->GetStringLength(text); + const jchar* jcharBuffer = env->GetStringChars(text,NULL); + if(jcharBuffer == NULL){ + return; + } + + WCHAR* unicodeBuffer = new WCHAR[len+1]; + memcpy(unicodeBuffer, jcharBuffer, len*sizeof(jchar)); + unicodeBuffer[len] = 0; + + pVoice->Speak(unicodeBuffer, + dwFlags, + NULL); + + /* release allocated memory */ + env->ReleaseStringChars(text,jcharBuffer); + delete [] unicodeBuffer; +} + +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1shutUp +(JNIEnv *env, jobject obj){ + pVoice->Speak(NULL,SPF_PURGEBEFORESPEAK,NULL); +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/WinNarrator/uk_ac_qmul_eecs_ccmi_speech_NativeWinNarrator.h Fri Dec 16 17:35:51 2011 +0000 @@ -0,0 +1,79 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <jni.h> +/* Header for class uk_ac_qmul_eecs_ccmi_speech_NativeNarrator */ + +#ifndef _Included_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator +#define _Included_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: uk_ac_qmul_eecs_ccmi_speech_NativeNarrator + * Method: _init + * Signature: ()Z + */ +JNIEXPORT jboolean JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1init + (JNIEnv *, jobject); + +/* + * Class: uk_ac_qmul_eecs_ccmi_speech_NativeNarrator + * Method: _dispose + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1dispose + (JNIEnv *, jobject); + +/* + * Class: uk_ac_qmul_eecs_ccmi_speech_NativeNarrator + * Method: _speak + * Signature: (Ljava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1speak + (JNIEnv *, jobject, jstring); + +/* + * Class: uk_ac_qmul_eecs_ccmi_speech_NativeNarrator + * Method: _speakWholeText + * Signature: (Ljava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1speakWholeText + (JNIEnv *, jobject, jstring); + +/* + * Class: uk_ac_qmul_eecs_ccmi_speech_NativeNarrator + * Method: _setRate + * Signature: (I)V + */ +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1setRate + (JNIEnv *, jobject, jint); + +/* + * Class: uk_ac_qmul_eecs_ccmi_speech_NativeNarrator + * Method: _shutUp + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_uk_ac_qmul_eecs_ccmi_speech_NativeNarrator__1shutUp + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif