Mercurial > hg > ccmieditor
changeset 3:9e67171477bc
PHANTOM Omni Heptic device release
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.txt Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,15 @@ +The CCmI Editor is a Collaborative Cross-Modal Diagram Editing Tool. + +Copyright of the source code belongs to Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) +and, where expressly specified, to Prof. Cay Horstmann (http://horstmann.com). + +The software is released under the GNU General Public License (version 3.0) with the exception of the code +in the "native/PhantomOmni" folder. The code in this folder, used to generate the native windows library +(.dll) that handles the PHANTOM Omni® Haptic Device, is a derivative work in accordance with the OpenHaptics® +Academic Edition License Agreement (see http://www.sensable.com/ for further information), a non-free license. + +Clearly the use of a non-free library is a drawback. If you are thinking of doing substantial further +work on the program, please first free it from dependence on the non-free library. + +PHANTOM Omni® and OpenHaptics® are registered trademarks of Sensable Technologies, Inc®. +
--- a/java/.classpath Mon Feb 06 12:54:06 2012 +0000 +++ b/java/.classpath Wed Apr 25 17:09:09 2012 +0100 @@ -3,6 +3,13 @@ <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/cmu_time_awb.jar"/> + <classpathentry kind="lib" path="libs/cmu_us_kal.jar"/> + <classpathentry kind="lib" path="libs/cmudict04.jar"/> + <classpathentry kind="lib" path="libs/cmulex.jar"/> + <classpathentry kind="lib" path="libs/cmutimelex.jar"/> + <classpathentry kind="lib" path="libs/en_us.jar"/> + <classpathentry kind="lib" path="libs/freetts.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"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTree.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,158 @@ +/* + 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.checkboxtree; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.SAXException; + + +/** + * A JTree containing {@code CheckBoxTreeNode} nodes. The tree is built according to an XML file + * passed as argument to the constructor. + * The XML file hierarchical structure reflects the structure of the tree. XML tag name can be + * either {@code selectable} or {@code unselectable}. The former representing tree nodes with a check + * box associated to it, and the latter a normal tree node, much as a {@link DefaultMutableTreeNode}. + * Either tags must have an attribute {@code value}, representing the name of the node which will be displayed + * in the tree. Here is an example of a simple XML file, representing a tree with the tree root (non selectable) having + * three children, the first of which has, in turn, a child. all the descendants of the root are selectable, but the + * first child. + * + * <pre> + * {@code + * <?xml version="1.0" encoding="utf-8"?> + * <unselectable value="root"> + * <unselectable value="first child"/> + * <selectable value="second child"/> + * <selectable value "third child"> + * <selectable value="grand child"/> + * </selectable> + * </unselectable> + * } + * </pre> + * + * @see CheckBoxTreeNode + */ +@SuppressWarnings("serial") +public class CheckBoxTree extends JTree { + public CheckBoxTree(InputStream stream, SetProperties values){ + super(new DefaultTreeModel(new DefaultMutableTreeNode())); + this.properties = values; + getAccessibleContext().setAccessibleName("tree"); + treeModel = (DefaultTreeModel)getModel(); + setCellRenderer(new CheckBoxTreeCellRenderer()); + buildTree(stream); + /* mouse listener to toggle the selected tree node */ + addMouseListener(new MouseAdapter(){ + @Override + public void mousePressed(MouseEvent e){ + TreePath path = getPathForLocation(e.getX(),e.getY()); + if(path == null) + return; + CheckBoxTreeNode treeNode = (CheckBoxTreeNode)path.getLastPathComponent(); + toggleSelection(treeNode); + } + }); + } + + /** + * Builds a CheckBoxTree out of an xml file passed as argument. All the nodes + * of the tree are marked unchecked. + * + * @param stream an input stream to an xml file + */ + public CheckBoxTree(InputStream stream){ + this(stream,null); + } + + /* use a sax parser to build the tree, if an error occurs during the parsing it just stops */ + private void buildTree(InputStream stream){ + SAXParserFactory factory = SAXParserFactory.newInstance(); + try { + SAXParser saxParser = factory.newSAXParser(); + XMLHandler handler = new XMLHandler((DefaultTreeModel)getModel(),properties); + saxParser.parse(stream, handler); + } catch (IOException e) { + e.printStackTrace(); + return; + } catch (ParserConfigurationException e) { + e.printStackTrace(); + return; + } catch (SAXException e) { + e.printStackTrace(); + return; + } + } + + /** + * Returns a reference to the properties holding which nodes of the tree are currently checked. + * @return the properties or {@value null} if the constructor with no properties was used. + */ + public SetProperties getProperties(){ + return properties; + } + + /** + * Toggle the check box of the tree node passed as argument. If the tree node + * is not a leaf, then all its descendants will get the new value it. That is, + * if the tree node becomes selected as a result of the call, then all the descendants + * will become in turn selected (regardless of their previous state). If it becomes + * unselected, the descendants will become unselected as well. + * + * @param treeNode the tree node to toggle + */ + public void toggleSelection(CheckBoxTreeNode treeNode){ + if(treeNode.isSelectable()){ + boolean selection = !treeNode.isSelected(); + treeNode.setSelected(selection); + if(selection) + properties.add(treeNode.getPathAsString()); + else + properties.remove(treeNode.getPathAsString()); + treeModel.nodeChanged(treeNode); + if(!treeNode.isLeaf()){ + for( @SuppressWarnings("unchecked") + Enumeration<CheckBoxTreeNode> enumeration = treeNode.depthFirstEnumeration(); enumeration.hasMoreElements();){ + CheckBoxTreeNode t = enumeration.nextElement(); + t.setSelected(selection); + if(selection) + properties.add(t.getPathAsString()); + else + properties.remove(t.getPathAsString()); + treeModel.nodeChanged(t); + } + } + } + } + + private SetProperties properties; + private DefaultTreeModel treeModel; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTreeCellRenderer.java Wed Apr 25 17:09:09 2012 +0100 @@ -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.checkboxtree; + +import java.awt.Component; + +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellRenderer; + + +/** + * A tree cell renderer which renders {@code CheckBoxTreeNode} objects like a {@code JCheckBox}: + * thick box (showing whether the node is selected or not) followed by a label (same label it would + * appear with a default {@code DefaultTreeCellRenderer}. + * + */ +@SuppressWarnings("serial") +public class CheckBoxTreeCellRenderer extends DefaultTreeCellRenderer { + + public CheckBoxTreeCellRenderer(){ + checkBox = new JCheckBox(); + checkBox.setBorder(BorderFactory.createLineBorder(this.getBorderSelectionColor())); + } + /** + * Returns the {@code Component} that the renderer uses to draw the value + * + * @throws ClassCastException if {@code value} is not an instance of {@code CheckoxTreeNode} + * @see javax.swing.tree.TreeCellRenderer#getTreeCellRendererComponent(JTree, Object, boolean, boolean, boolean, int, boolean) + * @return the {@code Component} that the renderer uses to draw the value + */ + @Override + public Component getTreeCellRendererComponent(JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus){ + + CheckBoxTreeNode treeNode = (CheckBoxTreeNode)value; + if(!treeNode.isSelectable()) + return super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + + checkBox.setSelected(treeNode.isSelected()); + checkBox.setText(value.toString()); + if(selected){ + checkBox.setBackground(getBackgroundSelectionColor()); + checkBox.setBorderPainted(true); + }else{ + checkBox.setBackground(getBackgroundNonSelectionColor()); + checkBox.setBorderPainted(false); + } + return checkBox; + } + + private JCheckBox checkBox; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTreeNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -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.checkboxtree; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; + + +/** + * A special {@code DefaultMutableTreeNode} that has, in addition, the property of being (or not) + * selected. + * + */ +@SuppressWarnings("serial") +public class CheckBoxTreeNode extends DefaultMutableTreeNode { + + /** + * Construct a new instance of this class. + * + * @param userObject a user object for this tree node @see javax.swing.tree#DefaultMutableTreeNode + * @param selectable whether or not this node is selectable. A non selectable node is pretty much + * equivalent to a {@code DefaultMutableTreeNode}. + */ + public CheckBoxTreeNode(Object userObject, boolean selectable){ + super(userObject); + this.selectable = selectable; + selected = false; + } + + /** + * Returns {@code true} if the node is selected, or {@code false} otherwise. + * + * @return {@code true} if the node is selected, or {@code false} otherwise. + */ + public boolean isSelected() { + return selected; + } + + /** + * Makes the node selected or unselected + * + * @param selected {@code true} to select the node, {@code false} to unselect it. + */ + public void setSelected(boolean selected) { + if(selectable){ + this.selected = selected; + } + } + + /** + * Whether the node is selectable or not. This depends on the value of the + * {@code selected} parameter passed to the constructor. + * + * @return {@code true} if the node is selectable, {@code false} otherwise. + */ + public boolean isSelectable(){ + return selectable; + } + + /** + * Returns a string representation of the audio description of this node. + * The value returned by this method can be passed to a Text-To-Speech synthesizer + * for speech rendering. + * + * @return the audio description of this node. + */ + public String spokenText(){ + if(selectable) + return toString()+", "+ (selected ? "checked": "unchecked"); + else + return toString(); + } + + /** + * Returns a string representation of the path of this tree node. The the string is made up + * as the concatenation of the tree node names from the root to this node, separated by + * {@link #STRING_PATH_SEPARATOR} + * + * @return the node path as a String + */ + public String getPathAsString(){ + StringBuilder builder = new StringBuilder(); + TreeNode [] path = getPath(); + for(int i=0; i<path.length; i++){ + builder.append(path[i].toString()); + if(i != path.length-1) + builder.append(STRING_PATH_SEPARATOR); + } + return builder.toString(); + } + + private boolean selected; + private boolean selectable; + /** + * The character used as a separator when returning the string representation of the path + * of this node via {@link #getPathAsString()}. + */ + public static final char STRING_PATH_SEPARATOR = '.'; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/SetProperties.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,206 @@ +/* + 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.checkboxtree; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * The {@code SetProperties} class represents a persistent set of properties. + * The {@code SetProperties} can be saved to a stream or loaded from a stream. + * + * Unlike {@code java.util.Properties}, this class is not backed by a key-value map, but rather + * by a {@code Set<String>}. Therefore it only contains values. All the methods of this class are thread-safe, + * but {@code iterator()}. In order to safely iterate on the Set, the iteration must happen in a block syncronized + * on the Object returned by {@link #getMonitor()}. + * + * For a description of the methods of the {@code Set} interface, see {@code java.util.Set} + * + * @see java.util.Properties + * @see java.util.Collections#synchronizedSet(Set) + */ +public class SetProperties implements Set<String> { + public SetProperties() { + delegate = Collections.synchronizedSet(new HashSet<String>()); + } + + public SetProperties(Collection<? extends String> collection) { + delegate = Collections.synchronizedSet(new HashSet<String>(collection)); + } + + public SetProperties(int initialCapacity) { + delegate = Collections.synchronizedSet(new HashSet<String>(initialCapacity)); + } + + public SetProperties(int initialCapacity, float loadFactor){ + delegate = Collections.synchronizedSet(new HashSet<String>(initialCapacity, loadFactor)); + } + + /* DELEGATE METHODS */ + @Override + public boolean add(String arg0) { + return delegate.add(arg0); + } + + @Override + public boolean addAll(Collection<? extends String> arg0) { + return delegate.addAll(arg0); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public boolean contains(Object arg0) { + return delegate.contains(arg0); + } + + @Override + public boolean containsAll(Collection<?> arg0) { + return delegate.containsAll(arg0); + } + + @Override + public boolean equals(Object arg0) { + return delegate.equals(arg0); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Iterator<String> iterator() { + return delegate.iterator(); + } + + @Override + public boolean remove(Object arg0) { + return delegate.remove(arg0); + } + + @Override + public boolean removeAll(Collection<?> arg0) { + return delegate.removeAll(arg0); + } + + @Override + public boolean retainAll(Collection<?> arg0) { + return delegate.retainAll(arg0); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + @Override + public <T> T[] toArray(T[] arg0) { + return delegate.toArray(arg0); + } + + /** + * Stores the content of this set (strings) in a text file. The content can then be retrieved + * by calling {@code load()} passing as argument the same file as this method. The strings + * will be written on a row each. + * + * @param file A valid File where the content of this object will be stored + * @param comments A comment string that will be added at the beginning of the file. + * + * @throws IOException if an exception occurs while writing the file + */ + public void store(File file, String comments) throws IOException{ + synchronized(delegate){ + if(file == null) + throw new IllegalArgumentException("File cannot be null"); + FileWriter fWriter = new FileWriter(file); + BufferedWriter writer = new BufferedWriter(fWriter); + if(comments != null){ + writer.write(COMMENTS_ESCAPE+" "+comments); + writer.newLine(); + writer.newLine(); + } + + for(String property : this){ + writer.write(property); + writer.newLine(); + } + writer.close(); + } + } + + /** + * Loads the content of a file into this set. Whaen the file is read, each row is taken as an + * entry of the set. + * + * @param file the file where to read the entries from + * @throws IOException if an exception occurs while reading the file + */ + public void load(File file) throws IOException{ + synchronized(delegate){ + if(file == null) + throw new IllegalArgumentException("File cannot be null"); + FileReader fReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fReader); + String line; + while((line = reader.readLine()) != null){ + if(!line.isEmpty() && !line.trim().startsWith(COMMENTS_ESCAPE)) + add(line); + } + reader.close(); + } + } + + /** + * Returns the Object all the methods (but {@code iterator()} of this {@code Set} are synchronized on. It can be used + * to safely iterate on this object without incurring in a race condition + * + * @return an {@code Object} to be used as synchronization monitor + * + * @see java.util.Collections#synchronizedSet(Set) + */ + public Object getMonitor(){ + return delegate; + } + + private Set<String> delegate; + private static String COMMENTS_ESCAPE = "#"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/XMLHandler.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,106 @@ +/* + 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.checkboxtree; + +import java.util.Stack; + +import javax.swing.tree.DefaultTreeModel; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/* Build a tree out of an XML file. The hierarchical structure of the XML file reflects + * the structure of the tree. The XMl file must have the form specified in CheckBoxTree + * class comment. + * + */ +class XMLHandler extends DefaultHandler { + + @SuppressWarnings("serial") + public XMLHandler(DefaultTreeModel treeModel, SetProperties properties){ + this.properties = properties; + this.treeModel = treeModel; + /* path is used to keep track of the current node's path. If the path is present as a * + * properties entry, then the current node, that has jus been created, will be set as selected */ + path = new Stack<String>() { + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(int i=0; i<size();i++){ + builder.append(get(i)); + if(i != size()-1) + builder.append(CheckBoxTreeNode.STRING_PATH_SEPARATOR); + } + return builder.toString(); + } + }; + } + + /* + * Create a CheckBoxTreeNode out of an xml tag. The tree node name is given by the value + * attribute. Whether the tree node is selectable or not, depends on the tag name, which can be + * selectable/unselectable. If the node is selectable, then it will be set as selected if its path + * is present in the properties + */ + @Override + public void startElement(String uri, + String localName, + String qName, + Attributes attributes) + throws SAXException { + String nodeName = attributes.getValue(VALUE_ATTR); + if(nodeName == null) + throw new SAXException("Value attribute missing"); + boolean isSelectable = SELECTABLE_NODE.equals(qName); + + CheckBoxTreeNode newNode = new CheckBoxTreeNode(nodeName,isSelectable); + if(currentNode == null){ + currentNode = newNode; + treeModel.setRoot(newNode); + }else{ + currentNode.add(newNode); + } + currentNode = newNode; + path.push(nodeName); + if(properties.contains(path.toString())) + newNode.setSelected(true); + } + + /* when an end tag is encountered, we carry on building the tree with the father as current node */ + @Override + public void endElement(String uri, + String localName, + String qName) + throws SAXException { + path.pop(); + currentNode = (CheckBoxTreeNode)currentNode.getParent(); + } + + private CheckBoxTreeNode currentNode; + private DefaultTreeModel treeModel; + private SetProperties properties; + private Stack<String> path; + /* attributes used in the XML file */ + public static final String VALUE_ATTR = "value"; + public static final String SELECTABLE_NODE = "selectable"; + public static final String UNSELECTABLE_NODE = "selectable"; + + +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionListener.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionListener.java Wed Apr 25 17:09:09 2012 +0100 @@ -26,9 +26,40 @@ */ public interface CollectionListener { + /** + * Called when either a node or an edge is inserted in the model. + * @param e an object representing the insertion event + */ void elementInserted(CollectionEvent e); + /** + * Called when either a node or an edge is removed in the model. + * @param e an object representing the remotion event + */ void elementTakenOut(CollectionEvent e); + /** + * Called when either a node or an edge in the model this listener is registered on. + * In order to identify which part of the element has been changed the call {@code e.getChangeType()} + * can be used. Furthermore the call {@code e.getArguments()} can return an object + * with additional information related to the change. + * The string returned by {@code e.getChangeType()} can be one of the following : + * <ul> + * <li>{@code name} : when the name of an element is changed + * <li>{@code properties} : when the all the properties of a node are changed all at once + * <li>{@code properties.clear} : when the all the properties of a node are deleted all at once + * <li>{@code property.add} : when a new property is added to a node. {@code e.getArguments()} will return + * a {@code PropertyChangeArgs} object, to retrieve the property from the node + * <li>{@code property.set} : when a property value changes into another value. {@code e.getArguments()} will return + * a {@code PropertyChangeArgs} object, to retrieve the property from the node + * <li>{@code property.remove} : when a property is removed from a node. + * <li>{@code property.modifiers} : when modifiers of a property are changed. {@code e.getArguments()} will return + * a {@code PropertyChangeArgs} object to retrieve the property from the node. {@code getOldValue()} will return in this + * case a String concatenation of all the modifier that were set before the change for this property. + * <li>{@code arrowHead} : when the arrow head of an edge is changed. + * <li>{@code endLabel} : + * </ul> + * @param e an object representing the change event + */ void elementChanged(ElementChangedEvent e); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionModel.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionModel.java Wed Apr 25 17:09:09 2012 +0100 @@ -19,6 +19,7 @@ package uk.ac.qmul.eecs.ccmi.diagrammodel; import java.util.Collection; +import java.util.concurrent.locks.ReentrantLock; import javax.swing.event.ChangeListener; @@ -44,40 +45,49 @@ /** * insert a DiagramNode into the diagram model * @param n the DiagramNode to be inserted in the collection + * @param source the source of the action. This will be reported as the source of the event + * generated by this action to the registered listeners. If null the CollectionModel instance + * itself will be used as source * @return true if this collection changed as a result of the call */ - boolean insert(N n) ; + boolean insert(N n, Object source) ; /** * insert a DiagramEdge into the diagram model * @param e the DiagramNode to be inserted in the collection + * @param source the source of the action. This will be reported as the source of the event + * generated by this action to the registered listeners. If null the CollectionModel instance + * itself will be used as source * @return true if this collection changed as a result of the call */ - boolean insert(E e); + boolean insert(E e, Object source); /** * Removes a DiagramElement from the model * @param e the diagramElement to be removed + * @param source the source of the action. This will be reported as the source of the event + * generated by this action to the registered listeners. If null the CollectionModel instance + * itself will be used as source * @return true if this collection changed as a result of the call */ - boolean takeOut(DiagramElement e); + boolean takeOut(DiagramElement e, Object source); /** - * Returns the diagram nodes contained by the model as a Collection + * 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 + * 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 + * of diagram elements. + * @return the collection of diagram elements */ Collection<DiagramElement> getElements(); @@ -116,11 +126,10 @@ 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 + * Returns a reentrant lock that can be used to access the nodes and edges via {@code getNodes()} + * and {@code getEdges()} and the change methods in a synchronized fashion. + * @return a lock object */ - Object getMonitor(); + ReentrantLock getMonitor(); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramEdge.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramEdge.java Wed Apr 25 17:09:09 2012 +0100 @@ -54,7 +54,7 @@ b.append(' ').append(spokenText()); b.append(". Connecting "); for(int i=0; i<getChildCount();i++){ - b.append(((DiagramModelTreeNode)getChildAt(i)).getName()); + b.append(((DiagramTreeNode)getChildAt(i)).getName()); b.append(and); } // remove the last " and " @@ -69,11 +69,11 @@ * @param n the node,at whose end the label is located * @param label the label */ - public void setEndLabel(DiagramNode n, String label){ + public void setEndLabel(DiagramNode n, String label, Object source){ if(label == null) label = ""; endLabels.put(n, label); - notifyChange(new ElementChangedEvent(n,this,"endLabel")); + notifyChange(new ElementChangedEvent(this,n,"endLabel",source)); } /** @@ -81,7 +81,6 @@ * 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); @@ -110,9 +109,9 @@ * as the empty string. * */ - public void setEndDescription(DiagramNode n, int index){ + public void setEndDescription(DiagramNode n, int index, Object source){ endDescriptions.put(n, index); - notifyChange(new ElementChangedEvent(n,this,"arrowHead")); + notifyChange(new ElementChangedEvent(this,n,"arrowHead",source)); } /** @@ -159,7 +158,7 @@ public abstract void connect(List<DiagramNode> nodes) throws ConnectNodesException; /** - * An index to be passed to {@link #setEndDescription(DiagramNode, int)} in order + * An index to be passed to {@link #setEndDescription(DiagramNode, int, Object)} 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;
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramElement.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramElement.java Wed Apr 25 17:09:09 2012 +0100 @@ -19,6 +19,8 @@ package uk.ac.qmul.eecs.ccmi.diagrammodel; import java.io.InputStream; +import java.util.concurrent.locks.ReentrantLock; + /** * A Diagram Element is either a node or an edge of the diagram. It's an abstract @@ -26,7 +28,7 @@ * */ @SuppressWarnings("serial") -public abstract class DiagramElement extends DiagramModelTreeNode implements Cloneable{ +public abstract class DiagramElement extends DiagramTreeNode implements Cloneable{ protected DiagramElement(){ name = ""; @@ -35,9 +37,10 @@ } /** - * 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. + * Returns the type of this diagram element. The type is like the category this element belongs to. + * For instance in a public transport diagram one might have three types of diagram element: tube, train + * and busses. + * * @return the type of this element */ public String getType(){ @@ -47,6 +50,7 @@ /** * 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){ @@ -76,9 +80,10 @@ /** * Sets the name of this element instance. * - * @param the string to set as the name + * @param s the string to set as the name + * @param source the source of this action */ - public void setName(String s){ + public void setName(String s, Object source){ String name = s; /* if the user enters an empty string we go back to the default name */ if(s.isEmpty() && id != NO_ID){ @@ -86,7 +91,7 @@ } setUserObject(name); this.name = name; - notifyChange(new ElementChangedEvent(this,this,"name")); + notifyChange(new ElementChangedEvent(this,this,"name",source)); } /** @@ -122,13 +127,19 @@ public long getId(){ return id; } + + public ReentrantLock getMonitor(){ + if(notifier == null) + return null; + return (ReentrantLock)notifier; + } /** * 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){ + <N extends DiagramNode,E extends DiagramEdge> void setNotifier(DiagramModel<N,E>.ReentrantLockNotifier notifier){ this.notifier = notifier; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModel.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModel.java Wed Apr 25 17:09:09 2012 +0100 @@ -1,6 +1,6 @@ /* 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 @@ -15,7 +15,7 @@ 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; @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; @@ -37,7 +38,6 @@ 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. @@ -63,42 +63,36 @@ */ @SuppressWarnings("serial") public DiagramModel(N [] nodePrototypes, E [] edgePrototypes) { - root = new DiagramModelTreeNode(ROOT_LABEL){ + root = new DiagramTreeNode(ROOT_LABEL){ @Override public boolean isRoot(){ return true; } }; modified = false; - + nodeCounter = 0; edgeCounter = 0; - + + notifier = new ReentrantLockNotifier(); + 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 * @@ -107,7 +101,7 @@ public CollectionModel<N,E> getDiagramCollection(){ return diagramCollection; } - + /** * Returns a TreeModel for this diagram * @@ -116,58 +110,64 @@ public TreeModel<N,E> getTreeModel(){ return treeModel; } - - private void triggerModification(Object source){ + + private void handleChangeListeners(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()); + DiagramTreeNode 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); + public boolean insert(N n, Object source){ + if(source == null) + source = this; + return _insert(n,source); } - + @Override - public boolean insert(E e){ - return _insert(e,this); + public boolean insert(E e, Object source){ + if(source == null) + source = this; + return _insert(e,source); } - + @Override - public boolean takeOut(DiagramElement element){ + public boolean takeOut(DiagramElement element, Object source){ + if(source == null) + source = this; if(element instanceof DiagramNode) - return _takeOut((DiagramNode)element,this); + return _takeOut((DiagramNode)element,source); if(element instanceof DiagramEdge) - return _takeOut((DiagramEdge)element,this); + return _takeOut((DiagramEdge)element,source); return false; } @@ -180,19 +180,19 @@ 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); @@ -208,71 +208,78 @@ public Collection<E> getEdges() { return Collections.unmodifiableCollection(edges); } - + @Override public Collection<DiagramElement> getElements(){ return Collections.unmodifiableCollection(elements); } @Override - public Object getMonitor(){ - return DiagramModel.this; + public ReentrantLock getMonitor(){ + return notifier; } - + @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; - } - + modified = false; + } + protected ArrayList<CollectionListener> listeners; } - - @SuppressWarnings("serial") + + @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); + public InnerTreeModel(TreeNode root){ + super(root); + bookmarks = new LinkedHashMap<String, DiagramTreeNode>(); + diagramTreeNodeListeners = new ArrayList<DiagramTreeNodeListener>(); } @Override - public boolean takeTreeNodeOut(DiagramElement treeNode){ + public boolean insertTreeNode(N treeNode, Object source){ + if(source == null) + source = this; + return _insert(treeNode,source); + } + + @Override + public boolean insertTreeNode(E treeNode, Object source){ + if(source == null) + source = this; + return _insert(treeNode,source); + } + + @Override + public boolean takeTreeNodeOut(DiagramElement treeNode, Object source){ + if(source == null) + source = this; boolean result; if(treeNode instanceof DiagramEdge){ - result = _takeOut((DiagramEdge)treeNode,this); + result = _takeOut((DiagramEdge)treeNode,source); } else{ - result = _takeOut((DiagramNode)treeNode,this); + result = _takeOut((DiagramNode)treeNode,source); } /* remove the bookmarks associated with the just deleted diagram element, if any */ for(String key : treeNode.getBookmarkKeys()) @@ -281,67 +288,86 @@ } @Override - public DiagramModelTreeNode putBookmark(String bookmark, DiagramModelTreeNode treeNode){ + public DiagramTreeNode putBookmark(String bookmark, DiagramTreeNode treeNode, Object source){ if(bookmark == null) throw new IllegalArgumentException("bookmark cannot be null"); - setEventSource(this); + if(source == null) + source = this; + setEventSource(source); treeNode.addBookmarkKey(bookmark); - DiagramModelTreeNode result = bookmarks.put(bookmark, treeNode); + DiagramTreeNode result = bookmarks.put(bookmark, treeNode); nodeChanged(treeNode); iLog("bookmark added",bookmark); - triggerModification(this); + DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,bookmark,source); + for(DiagramTreeNodeListener l : diagramTreeNodeListeners){ + l.bookmarkAdded(evt); + } + handleChangeListeners(this); return result; } @Override - public DiagramModelTreeNode getBookmarkedTreeNode(String bookmark) { + public DiagramTreeNode getBookmarkedTreeNode(String bookmark) { return bookmarks.get(bookmark); } - + @Override - public DiagramModelTreeNode removeBookmark(String bookmark) { - setEventSource(this); - DiagramModelTreeNode treeNode = bookmarks.remove(bookmark); + public DiagramTreeNode removeBookmark(String bookmark,Object source) { + if(source == null) + source = this; + setEventSource(source); + DiagramTreeNode treeNode = bookmarks.remove(bookmark); treeNode.removeBookmarkKey(bookmark); nodeChanged(treeNode); iLog("bookmark removed",bookmark); - triggerModification(this); + DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,bookmark,source); + for(DiagramTreeNodeListener l : diagramTreeNodeListeners){ + l.bookmarkRemoved(evt); + } + handleChangeListeners(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); + public void setNotes(DiagramTreeNode treeNode, String notes,Object source){ + if(source == null) + source = this; + setEventSource(source); + String oldValue = treeNode.getNotes(); + treeNode.setNotes(notes,source); nodeChanged(treeNode); iLog("notes set for "+treeNode.getName(),"".equals(notes) ? "empty notes" : notes.replaceAll("\n", "\\\\n")); - triggerModification(this); + DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,oldValue,source); + for(DiagramTreeNodeListener l : diagramTreeNodeListeners){ + l.notesChanged(evt); + } + handleChangeListeners(source); } - + private void setEventSource(Object source){ this.src = source; } - + @Override - public Object getMonitor(){ - return DiagramModel.this; + public ReentrantLock getMonitor(){ + return notifier; } - + @Override - public void addChangeListener(ChangeListener l){ - DiagramModel.this.addChangeListener(l); + public void addDiagramTreeNodeListener(DiagramTreeNodeListener l){ + diagramTreeNodeListeners.add(l); } - + @Override - public void removeChangeListener(ChangeListener l){ - DiagramModel.this.removeChangeListener(l); + public void removeDiagramTreeNodeListener(DiagramTreeNodeListener l){ + diagramTreeNodeListeners.remove(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 @@ -367,50 +393,60 @@ int[] childIndices, Object[] children) { super.fireTreeStructureChanged(src, path, childIndices, children); } - + public boolean isModified(){ return modified; } - + public void setUnmodified(){ - modified = false; - } - + modified = false; + } + private Object src; - private Map<String, DiagramModelTreeNode> bookmarks; - } - - private synchronized boolean _insert(N n, Object source) { - assert(n != null); + private Map<String, DiagramTreeNode> bookmarks; + private ArrayList<DiagramTreeNodeListener> diagramTreeNodeListeners; + } + + @SuppressWarnings("serial") + class ReentrantLockNotifier extends ReentrantLock implements ElementNotifier { + @Override + public void notifyChange(ElementChangedEvent evt) { + _change(evt); + handleChangeListeners(evt.getDiagramElement()); + } + } - /* 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); + private 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()); + DiagramTreeNode 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); @@ -419,15 +455,15 @@ /* this is necessary to increment the child counter displayed between brackets */ treeModel.nodeChanged(parent); diagramCollection.fireElementInserted(source,n); - triggerModification(n); - + handleChangeListeners(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 */ + } + + private 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); @@ -440,8 +476,8 @@ elements.remove(n); /* notify all the listeners a new node has been removed */ diagramCollection.fireElementTakenOut(source,n); - triggerModification(n); - + handleChangeListeners(n); + if(nodes.isEmpty()){ nodeCounter = 0; }else{ @@ -451,125 +487,125 @@ } 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 */ + } + + private 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("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()); + DiagramTreeNode 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*/ + + /* 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); + 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()); + + DiagramTreeNode 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); - + handleChangeListeners(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); + } + + private 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 */ + _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(); + /* notify listeners for collection */ + diagramCollection.fireElementTakenOut(source,e); + handleChangeListeners(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; - } + } + 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 */ + 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); + DiagramTreeNode 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 */ @@ -580,202 +616,203 @@ edgesToRemove.add(e); }else{ e.removeNode(n); - DiagramModelTreeNode nodeTreeReference = _lookForNodeReference(e, n); + DiagramTreeNode 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); + } + + /* the tree structure is changed as a consequence of this method, therefore it's synchronized * + * so that external classes accessing the tree can get exclusive access through getMonitor() */ + private void _change(ElementChangedEvent evt){ + synchronized(this){ + 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.getDiagramElement() instanceof DiagramNode){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + 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.getDiagramElement(); + 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(); + ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType()); + PropertyMutableTreeNode propertyNode = new PropertyMutableTreeNode(n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex())); + typeNode.add(propertyNode); + treeModel.insertNodeInto(propertyNode, typeNode, args.getPropertyIndex()); + /* 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(); + ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType()); + ((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex())) + .setUserObject(n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex())); + treeModel.nodeChanged((typeNode.getChildAt(args.getPropertyIndex()))); + iLog("property changed",n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex())); + }else if("property.remove".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType()); + iLog("property removed",((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex())).getName()); //must do it before actual removing + treeModel.removeNodeFromParent((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex())); + /* 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(); + ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType()); + PropertyMutableTreeNode propertyNode = ((PropertyMutableTreeNode)typeNode.getChildAt(args.getPropertyIndex())); + StringBuilder builder = new StringBuilder(); + Modifiers modifiers = n.getProperties().getModifiers(args.getPropertyType()); + for(int index : modifiers.getIndexes(args.getPropertyIndex())) + 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.getArguments(); + 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 */ + } - 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();){ + } + /* do nothing for other ElementChangedEvents as they only concern the diagram listeners */ + /* just forward the event to other listeners which might have been registered */ + diagramCollection.fireElementChanged(evt); + } + + private static DiagramTreeNode _lookForChild(DiagramTreeNode parentNode, String name){ + DiagramTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramTreeNode> children = parentNode.children(); children.hasMoreElements();){ temp = children.nextElement(); if(temp.getName().equals(name)){ - child = temp; - break; + 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; + return child; + } + + private static NodeReferenceMutableTreeNode _lookForNodeReference(DiagramEdge parent, DiagramNode n){ + NodeReferenceMutableTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramTreeNode> 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){ + DiagramTreeNode edgeType = _lookForChild(parentNode, e.getType()); + assert(edgeType != null); + EdgeReferenceMutableTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramTreeNode> 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 DiagramTreeNode root; + private 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 ReentrantLockNotifier 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;}
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModelTreeNode.java Mon Feb 06 12:54:06 2012 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,289 +0,0 @@ -/* - 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(); - } - } -}
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramNode.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -18,10 +18,11 @@ */ package uk.ac.qmul.eecs.ccmi.diagrammodel; +import java.util.List; import java.util.Set; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent.PropertyChangeArgs; 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. @@ -35,15 +36,6 @@ } /** - * @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. @@ -77,9 +69,9 @@ * Set the NodeProperties of this node * @param properties the properties to set for this node */ - public void setProperties(NodeProperties properties){ + public void setProperties(NodeProperties properties, Object source){ this.properties = properties; - notifyChange(new ElementChangedEvent(this.properties,this,"properties")); + notifyChange(new ElementChangedEvent(this,this.properties,"properties",source)); } /** @@ -87,46 +79,54 @@ * @see NodeProperties#addValue(String, String) * */ - public void addProperty(String propertyType, String propertyValue){ + public void addProperty(String propertyType, String propertyValue, Object source){ getProperties().addValue(propertyType, propertyValue); int index = getProperties().getValues(propertyType).size() - 1; - notifyChange(new ElementChangedEvent(new Pair<String,Integer>(propertyType,index),this,"property.add")); + notifyChange(new ElementChangedEvent(this,new PropertyChangeArgs(propertyType,index,""),"property.add",source)); } /** * Removes a property from the NodeProperties of this node * @see NodeProperties#removeValue(String, int) */ - public void removeProperty(String propertyType, int valueIndex){ + public void removeProperty(String propertyType, int valueIndex, Object source){ + String oldValue = getProperties().getValues(propertyType).get(valueIndex); getProperties().removeValue(propertyType, valueIndex); - notifyChange(new ElementChangedEvent(new Pair<String,Integer>(propertyType,valueIndex),this,"property.remove")); + notifyChange(new ElementChangedEvent(this,new PropertyChangeArgs(propertyType,valueIndex,oldValue),"property.remove",source)); } /** * 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){ + public void setProperty(String propertyType, int valueIndex, String newValue, Object source){ + String oldValue = getProperties().getValues(propertyType).get(valueIndex); getProperties().setValue(propertyType, valueIndex, newValue); - notifyChange(new ElementChangedEvent(new Pair<String,Integer>(propertyType,valueIndex),this,"property.set")); + notifyChange(new ElementChangedEvent(this,new PropertyChangeArgs(propertyType,valueIndex,oldValue),"property.set",source)); } /** * Removes all the values in the NodeProperties of this node * @see NodeProperties#clear() */ - public void clearProperties(){ + public void clearProperties(Object source){ getProperties().clear(); - notifyChange(new ElementChangedEvent(this,this,"properties.clear")); + notifyChange(new ElementChangedEvent(this,this,"properties.clear",source)); } /** * 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){ + public void setModifierIndexes(String propertyType, int propertyValueIndex, Set<Integer> modifierIndexes,Object source){ + StringBuilder oldIndexes = new StringBuilder(); + List<String> modifierTypes = getProperties().getModifiers(propertyType).getTypes(); + Set<Integer> indexes = getProperties().getModifiers(propertyType).getIndexes(propertyValueIndex); + for(Integer I : indexes){ + oldIndexes.append(modifierTypes.get(I)).append(' '); + } getProperties().getModifiers(propertyType).setIndexes(propertyValueIndex, modifierIndexes); - notifyChange(new ElementChangedEvent(new Pair<String,Integer>(propertyType,propertyValueIndex),this,"property.modifiers")); + notifyChange(new ElementChangedEvent(this,new PropertyChangeArgs(propertyType,propertyValueIndex,oldIndexes.toString()),"property.modifiers",source)); } /** @@ -143,7 +143,7 @@ builder.append(getName()); builder.append('.').append(' '); for(int i=0; i<getChildCount();i++){ - DiagramModelTreeNode treeNode = (DiagramModelTreeNode) getChildAt(i); + DiagramTreeNode treeNode = (DiagramTreeNode) getChildAt(i); if(treeNode.getChildCount() > 0){ builder.append(treeNode.getChildCount()) .append(' ') @@ -170,13 +170,13 @@ /** * add an edge to the attached edges list - * @param the edge to be added + * @param e 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 + * @param e the edge to be removed */ public abstract boolean removeEdge(DiagramEdge e);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramTreeNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,292 @@ +/* + 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 DiagramTreeNode extends DefaultMutableTreeNode { + /** + * Creates a tree node with the default user object. The default user object has no label. Therefore + * this node will have no label when displayed on a tree. + */ + public DiagramTreeNode() { + super(); + notes = ""; + userObject = new UserObject(); + setSuperClassUserObject(userObject); + bookmarkKeys = new ArrayList<String>(); + } + + /** + * Creates a tree node, holding the user object passed as argument. The label of the + * tree node will be the string returned by {@code userObject.toString()} + * + * @param userObject the user object for this tree node + * + * @see javax.swing.tree.DefaultMutableTreeNode + */ + public DiagramTreeNode(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 bookmark keys currently associated to this tree node in the tree model. + * + * @return a n unmodifiable list of strings used as keys for bookmarks + */ + 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 + * @param source used by {@code DiagramElement} to trigger {@code ElementChangeEvents} + * + * @see DiagramElement#setNotes(String, Object) + */ + protected void setNotes(String note, Object source){ + this.notes = note; + } + + @Override + public DiagramTreeNode getParent(){ + return (DiagramTreeNode)super.getParent(); + } + + @Override + public DiagramTreeNode getChildAt(int i){ + return (DiagramTreeNode)super.getChildAt(i); + } + + @Override + public DiagramTreeNode getRoot(){ + return (DiagramTreeNode)super.getRoot(); + } + + @Override + public void setUserObject(Object userObject){ + ((UserObject)this.userObject).setObject(userObject); + } + + @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(); + } + + @Override + public boolean isRoot(){ + return false; // root node overwrites this method + } + + @Override + public DiagramTreeNode getLastLeaf() { + return (DiagramTreeNode)super.getLastLeaf(); + } + + @Override + public DiagramTreeNode getNextLeaf() { + return (DiagramTreeNode)super.getNextLeaf(); + } + + @Override + public DiagramTreeNode getNextNode() { + return (DiagramTreeNode)super.getNextNode(); + } + + @Override + public DiagramTreeNode getNextSibling() { + return (DiagramTreeNode)super.getNextSibling(); + } + + @Override + public DiagramTreeNode getPreviousLeaf() { + return (DiagramTreeNode)super.getPreviousLeaf(); + } + + @Override + public DiagramTreeNode getPreviousNode() { + return (DiagramTreeNode)super.getPreviousNode(); + } + + @Override + public DiagramTreeNode getPreviousSibling() { + return (DiagramTreeNode)super.getPreviousSibling(); + } + + private void setSuperClassUserObject(Object u){ + super.setUserObject(u); + } + + private UserObject getUserObjectInstance(){ + return new UserObject(); + } + + /** + * The bookmarks, involving this node, entered by the user in the DiagramTree this node belongs to. + */ + protected List<String> bookmarkKeys; + /** + * The notes set by the user for this node. + */ + protected String notes; + /* hides the DefaultMutableTreeNode protected field */ + private Object userObject; + /** + * The character that is appended to the label of this node when the user enters some notes for it. + */ + protected static final char NOTES_CHAR = '#'; + /** + * The character that is appended to the label of this node when it's bookmarked by the user. + */ + protected static final char BOOKMARK_CHAR = '\''; + /** + * The string that is appended to the spoken text of this node when the user enters some notes for it. + * + * @see #spokenText() + */ + protected static final String BOOKMARK_SPEAK = ", bookmarked"; + /** + * The string that is appended to the spoken text of this node when it's bookmarked by the user. + * + * @see #spokenText() + */ + protected static final String NOTES_SPEAK = ", has notes"; + + @Override + public Object clone(){ + DiagramTreeNode clone = (DiagramTreeNode )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/DiagramTreeNodeEvent.java Wed Apr 25 17:09:09 2012 +0100 @@ -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.diagrammodel; + +import java.util.EventObject; + +/** + * An event that is triggered in the {@code DiagramModel} when an action + * happens that can involve all the tree nodes of the model's {@code DiagramTree} + * rather than only nodes or edges. + * + * Particularly such actions happens when the user set the notes for a tree node or when + * they bookmark a tree node. + * + */ +@SuppressWarnings("serial") +public class DiagramTreeNodeEvent extends EventObject { + /** + * Creates a new event related to a change in a tree node on either notes or bookmarks. + * In order to get the status of the tree node before the change the old value is passed as + * parameter to this constructor. + * For {@code setNotes} this value is the notes before the change. For {@code removeBookmark} it + * will be the bookmark that has just been removed. Finally for {@code putBookmark()} the value is the + * new bookmark. + * + * @see uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel + * + * @param treeNode the tree node where the action happened + * @param value the value that the tree node had before this action. + * @param source the source of the action + */ + public DiagramTreeNodeEvent(DiagramTreeNode treeNode, String value, Object source){ + super(source); + this.treeNode = treeNode; + this.value = value; + } + + /** + * Returns the tree node on which a new note has been set or a bookmark has been added/removed. + * + * @return the tree node this event refers to. + */ + public DiagramTreeNode getTreeNode() { + return treeNode; + } + + /** + * Returns the old value before the action (the old notes for {@code setNotes()}, + * and the bookmark key for {@code putBookmark() and removeBookmark()}. + * + * @return the old value before the action + */ + public String getValue() { + return value; + } + + private DiagramTreeNode treeNode; + private String value; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramTreeNodeListener.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,49 @@ +/* + 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 listener interface for receiving {@code DiagramTreeNode} events. This events + * happen when the user changes the state of any tree node in the {@code DiagramTree}, + * not necessarily (but possibly) a {@code DiagramNode} or {@code DiagramEdge}. + * + */ +public interface DiagramTreeNodeListener { + + /** + * Called when a new bookmark is added to {@code DiagramTree}. + * + * @param evt the event object representing a new bookmark insertion in the {@code DiagramTree}. + */ + public void bookmarkAdded(DiagramTreeNodeEvent evt); + + /** + * Called when a bookmark is removed from the {@code DiagramTree}. + * + * @param evt the event object representing a new bookmark insertion in the {@code DiagramTree}. + */ + public void bookmarkRemoved(DiagramTreeNodeEvent evt); + + /** + * Called when a note is set on a node in the {@code DiagramTree}. + * + * @param evt evt the event object representing a note set on a node in the {@code DiagramTree}. + */ + public void notesChanged(DiagramTreeNodeEvent evt); +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceHolderMutableTreeNode.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceHolderMutableTreeNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -20,14 +20,12 @@ /** * 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. - * - * + * in the tree layout, where It is normally placed as a {@code DiagramNode}'s child. */ @SuppressWarnings("serial") -public class EdgeReferenceHolderMutableTreeNode extends DiagramModelTreeNode { +public class EdgeReferenceHolderMutableTreeNode extends DiagramTreeNode { - public EdgeReferenceHolderMutableTreeNode(Object userObj){ + EdgeReferenceHolderMutableTreeNode(Object userObj){ super(userObj); } @@ -39,8 +37,9 @@ } /** - * 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(). + * Returns a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than {@code toString()}. + * * @return a String suitable for text to speech synthesis */ @Override
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceMutableTreeNode.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceMutableTreeNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -19,14 +19,12 @@ 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){ +public class EdgeReferenceMutableTreeNode extends DiagramTreeNode { + EdgeReferenceMutableTreeNode(DiagramEdge edge, DiagramNode node){ super(); this.edge = edge; this.node = node;
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ElementChangedEvent.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ElementChangedEvent.java Wed Apr 25 17:09:09 2012 +0100 @@ -21,17 +21,28 @@ 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). + * ElementChangedEvent is used to notify the model listeners that an element ({@code DiagramNode} + * or {@code DiagramEdge}) 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) { + /** + * Creates a new instance of {@code ElementChangedEvent} + * + * @param element the element that has been changed + * @param args + * @param changeType it's a {@code String} identifying the change type. Subclasses of {@code DiagramNode} and + * {@code DiagramEdge} can define their own change events and and make listeners aware of them via their + * {@code notifyChnage()} method. Listeners (defined outside this package as well) can then identify such changes using this string. + * @param source the source of the change that triggered this event + */ + public ElementChangedEvent( DiagramElement element, Object args, String changeType, Object source) { super(source); this.changeType = changeType; this.element = element; + this.arguments = args; } /** @@ -53,6 +64,46 @@ return element; } + /** + * Returns the arguments of the change if the the change type has any + * + * @return an object representing the arguments or null + */ + public Object getArguments(){ + return arguments; + } + private String changeType; - DiagramElement element; + private DiagramElement element; + private Object arguments; + + /** + * This class is returned by {@link ElementChangedEvent#getArguments()} when a node property is + * changed. It holds the informations about the property type and the property index, so that + * listeners can retrieve the property when handling the event. + * + */ + public static class PropertyChangeArgs { + public PropertyChangeArgs(String type, int index, String oldValue){ + this.type = type; + this.index = index; + this.oldValue = oldValue; + } + + public String getPropertyType(){ + return type; + } + + public int getPropertyIndex(){ + return index; + } + + public String getOldValue(){ + return oldValue; + } + + private String type; + private String oldValue; + private int index; + } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/MalformedEdgeException.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/MalformedEdgeException.java Wed Apr 25 17:09:09 2012 +0100 @@ -1,3 +1,21 @@ +/* + 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/>. +*/ /* CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool @@ -26,11 +44,13 @@ @SuppressWarnings("serial") public class MalformedEdgeException extends RuntimeException { - public MalformedEdgeException(DiagramEdge edge, String message) { + /** + * Creates a new {@code MalformedEdgeException} holding the messsage passed as argument. + * The message can be accessed by calling {@code getMessage()} on this exception. + * + * @param message + */ + public MalformedEdgeException(String message) { super("Edge inserted into data structure was malformed for this reason:" + message); } - - public MalformedEdgeException(Throwable arg0) { - super(arg0); - } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeProperties.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeProperties.java Wed Apr 25 17:09:09 2012 +0100 @@ -88,17 +88,17 @@ /** * Returns the types of properties. - * @return + * + * @return a list of strings holding types of properties */ public List<String> getTypes(){ return types; } /** - * - * @param types the property type we want to get the values of. + * @param propertyType 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. + * {@code null} if the specified type does not exist. */ public List<String> getValues(String propertyType){ Entry e = properties.get(propertyType); @@ -110,7 +110,8 @@ /** * 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. + * 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 */ @@ -124,7 +125,8 @@ /** * 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 + * its own view. + * * @param type the type of property the view is associated with. * @param o an object defined by user * @see #getView(String) @@ -137,7 +139,6 @@ } /** - * * 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. * @@ -154,7 +155,8 @@ } /** - * Adds a value of the type passed as argument + * 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 */ @@ -169,7 +171,8 @@ /** * Adds a value of the type passed as argument and sets the modifier indexes for it as for - * {@link Modifiers#setIndexes(int, Set)} + * {@link Modifiers#setIndexes(int, Set)}. + * * @param propertyType a property type * @param propertyValue a property value * @param modifierIndexes the modifier set of indexes @@ -192,7 +195,8 @@ } /** - * Removes the value at the specified index for the specified property type + * 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 @@ -208,11 +212,12 @@ } /** - * Sets the value of a property type at the specified index to a new value + * 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 + * @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){ @@ -223,7 +228,8 @@ } /** - * Removes all the values and modifiers for a specific type + * 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 @@ -246,7 +252,8 @@ } /** - * Returns true if this NodeProperties contains no values + * Returns true if this NodeProperties contains no values. + * * @return true if this NodeProperties contains no values */ public boolean isEmpty(){ @@ -260,7 +267,8 @@ } /** - * Returns true if there are no values for the specified type in this NodeProperties instance + * 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 */ @@ -273,7 +281,8 @@ /** * 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 + * has been called with an empty or null property type definition. + * * @return true if this NodeProperties object has no types */ public boolean isNull(){ @@ -282,7 +291,7 @@ /** * 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 + * representation can be passed as argument to {@link #fill(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. * @@ -309,7 +318,7 @@ /** * Fills up the this property according to the string passed as arguments - * The string must be generated by calling toString on a NodeProeprty instance + * The string must be generated by calling toString on a NodeProperty instance * holding the same property types as type, otherwise an IllegalArgumentException * is likely to be thrown. * @@ -318,7 +327,7 @@ * @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){ + public void fill(String s){ /* clear up previous values */ clear(); @@ -411,6 +420,7 @@ /** * 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) */ @@ -422,7 +432,8 @@ * 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 + * + * @param modifierType 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){ @@ -433,7 +444,8 @@ * 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 modifierType the type of modifier the view is associated with. * @param o an object defined by user * @see #getView(String) * @throws IllegalArgumentException if modifierType @@ -462,6 +474,7 @@ /** * 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 @@ -482,7 +495,8 @@ } /** - * Removes all the modifier indexes for a property value at the specified index + * 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){ @@ -493,7 +507,8 @@ /** * 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 + * to the constructor of the NodeProperties object holding this Modifiers object. + * * @return true if this Modifiers object has no types */ public boolean isNull(){ @@ -517,7 +532,7 @@ /** * 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 + * @see #isNull() */ public static final NodeProperties NULL_PROPERTIES = new NodeProperties(EMPTY_PROPERTY_TYPE_DEFINITION);
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeReferenceMutableTreeNode.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeReferenceMutableTreeNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -19,15 +19,12 @@ package uk.ac.qmul.eecs.ccmi.diagrammodel; /** - * - * The diagramModeltreeNode placed in an edge subtree representing a node connected + * The {@code 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){ +public class NodeReferenceMutableTreeNode extends DiagramTreeNode { + NodeReferenceMutableTreeNode(DiagramNode node, DiagramEdge edge){ super(); this.node = node; this.edge = edge; @@ -55,8 +52,9 @@ } /** - * returns the tree node name "as it is", without any decoration such as notes, bookmarks or cardinality. - * Unlike the String returned by 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 @@ -67,6 +65,7 @@ /** * 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 @@ -78,6 +77,7 @@ /** * 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(){ @@ -85,7 +85,8 @@ } /** - * Returns the diagram node that this tree node represents inside the edge subtree + * Returns the diagram node that this tree node represents inside the edge subtree. + * * @return a reference to the actual diagram node */ public DiagramNode getNode(){
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyMutableTreeNode.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyMutableTreeNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -24,13 +24,12 @@ * @see NodeProperties */ @SuppressWarnings("serial") -public class PropertyMutableTreeNode extends DiagramModelTreeNode { - public PropertyMutableTreeNode(){ +public class PropertyMutableTreeNode extends DiagramTreeNode { + PropertyMutableTreeNode(){ super(); } - - public PropertyMutableTreeNode(Object userObject) { + PropertyMutableTreeNode(Object userObject) { super(userObject); modifiersString = ""; } @@ -52,7 +51,8 @@ /** * 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(). + * for a text to speech synthesizer to read, than toString(). + * * @return a String suitable for text to speech synthesis */ @Override
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyTypeMutableTreeNode.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyTypeMutableTreeNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -24,24 +24,33 @@ 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. + * This {@code DiagramModelTreeNode} holds all the {@code PropertyMutableTreeNode} instances + * of a certain type in the diagram tree. * * @see PropertyMutableTreeNode - * */ @SuppressWarnings("serial") -public class PropertyTypeMutableTreeNode extends DiagramModelTreeNode { - public PropertyTypeMutableTreeNode(String type, DiagramNode n){ +public class PropertyTypeMutableTreeNode extends DiagramTreeNode { + PropertyTypeMutableTreeNode(String type, DiagramNode n){ setUserObject(type); node = n; } + /** + * Returns the property type this tree node is related to. + * + * @return a property type string + */ public String getType(){ return getName(); } + /** + * Returns a reference to the node that's holding a {@code NodeProperties} with + * properties of this type. + * + * @return a reference to a {@code DiagramNode} object + */ public DiagramNode getNode(){ return node; } @@ -97,5 +106,4 @@ } DiagramNode node; - }
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TreeModel.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TreeModel.java Wed Apr 25 17:09:09 2012 +0100 @@ -19,8 +19,7 @@ package uk.ac.qmul.eecs.ccmi.diagrammodel; import java.util.Set; - -import javax.swing.event.ChangeListener; +import java.util.concurrent.locks.ReentrantLock; /** * @@ -35,25 +34,34 @@ * insert a DiagramNode into the diagram model * * @param treeNode the DiagramNode to be inserted in the collection + * @param source the source of the action. This will be reported as the source of the event + * generated by this action to the registered listeners. If null the TreeModel instance + * itself will be used as source * @return true if the model changed as a result of the call */ - boolean insertTreeNode(N treeNode); + boolean insertTreeNode(N treeNode, Object source); /** * insert a DiagramEdge into the diagram model * * @param treeNode the DiagramEdge to be inserted in the collection + * @param source the source of the action. This will be reported as the source of the event + * generated by this action to the registered listeners. If null the TreeModel instance + * itself will be used as source * @return true if the model changed as a result of the call */ - boolean insertTreeNode(E treeNode); + boolean insertTreeNode(E treeNode, Object source); /** * remove a DiagramElement from the model * * @param treeNode the diagramElement to be removed + * @param source the source of the action. This will be reported as the source of the event + * generated by this action to the registered listeners. If null the TreeModel instance + * itself will be used as source * @return true if the model changed as a result of the call */ - boolean takeTreeNodeOut(DiagramElement treeNode); + boolean takeTreeNodeOut(DiagramElement treeNode, Object source); /** * @@ -64,14 +72,14 @@ * @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); + DiagramTreeNode putBookmark(String bookmark, DiagramTreeNode treeNode, Object source); /** * 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); + DiagramTreeNode getBookmarkedTreeNode(String bookmark); /** * Returns the list of all the bookmarks of this tree model @@ -85,7 +93,7 @@ * @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); + DiagramTreeNode removeBookmark(String bookmark, Object source); /** * Set the notes for the specified tree node. Passing an empty string as notes @@ -93,22 +101,24 @@ * * @param treeNode the tree node to be noted * @param notes the notes to be assigned to the tree node + * @param source the source of the action. This will be reported as the source of the event + * generated by this action to the registered listeners */ - void setNotes(DiagramModelTreeNode treeNode, String notes); + void setNotes(DiagramTreeNode treeNode, String notes, Object source); /** - * Add a change listener to the model. the listeners will be fired each time the model + * Add a {@code DiagramNodeListener} to this object. 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 + * @param l a {@code DiagramNodeListener} to add to the model */ - void addChangeListener(ChangeListener l); + void addDiagramTreeNodeListener(DiagramTreeNodeListener l); /** - * Removes a change listener from the model. - * @param l a ChangeListener to remove from the model + * Removes a {@code DiagramNodeListener} from this object. + * @param l a {@code DiagramNodeListener} to remove from ththis object. */ - void removeChangeListener(ChangeListener l); + void removeDiagramTreeNodeListener(DiagramTreeNodeListener l); /** * Returns true if the model has been modified @@ -124,10 +134,8 @@ 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 + * Returns a reentrant lock that can be used to access the nodes and edges in a synchronized fashion. + * @return a lock object */ - Object getMonitor(); + public ReentrantLock getMonitor(); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TypeMutableTreeNode.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TypeMutableTreeNode.java Wed Apr 25 17:09:09 2012 +0100 @@ -25,9 +25,8 @@ * */ @SuppressWarnings("serial") -public class TypeMutableTreeNode extends DiagramModelTreeNode { - - public TypeMutableTreeNode(DiagramElement element) { +public class TypeMutableTreeNode extends DiagramTreeNode { + TypeMutableTreeNode(DiagramElement element) { super(element.getType()); this.prototype = element; } @@ -66,5 +65,5 @@ return builder.toString(); } - DiagramElement prototype; + private DiagramElement prototype; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/AudioFeedback.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,163 @@ +/* + 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.text.MessageFormat; +import java.util.ResourceBundle; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionListener; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNodeListener; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNodeEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent.PropertyChangeArgs; +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; + +/** + * This class is a listener providing audio (speech + sound) feedback to changes on the + * model (e.g. node added, node removed, node name changed etc.) operated only on the local (so not from + * a tree of another user sharing the same diagram) + * tree it is linked to. If the source of the events is different from the local tree , then no action + * is performed. + */ +public class AudioFeedback implements CollectionListener, DiagramTreeNodeListener { + + /** + * Construct an {@code AudioFeedback} object linked to a {@code DiagramTree}. + * + * @param tree the tree this instance is going to be linked to + */ + AudioFeedback(DiagramTree tree){ + resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + this.tree = tree; + } + + @Override + public void elementInserted(CollectionEvent e) { + DiagramEventSource source = (DiagramEventSource)e.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + final DiagramElement diagramElement = e.getDiagramElement(); + boolean isNode = diagramElement instanceof Node; + if(isNode){ + SoundFactory.getInstance().play( SoundEvent.OK ,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.input.node.ack"),diagramElement.spokenText())); + } + }); + }else{ + Edge edge = (Edge)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().play( SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(builder.toString()); + } + }); + } + } + } + + @Override + public void elementTakenOut(CollectionEvent e) { + DiagramEventSource source = (DiagramEventSource)e.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + final DiagramElement element = e.getDiagramElement(); + SoundFactory.getInstance().play(SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.delete.element.ack"),element.spokenText(),tree.currentPathSpeech())); + } + }); + } + } + + @Override + public void elementChanged(ElementChangedEvent e) { + DiagramEventSource source = (DiagramEventSource)e.getSource(); + if(!source.isLocal() || source.type != DiagramEventSource.Type.TREE) + return; + String change = e.getChangeType(); + if("name".equals(change)){ + playOK(tree.currentPathSpeech()); + }else if ("property.add".equals(change)){ + PropertyChangeArgs args = (PropertyChangeArgs)e.getArguments(); + String propertyValue = ((Node)e.getDiagramElement()).getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex()); + playOK(MessageFormat.format(resources.getString("speech.input.property.ack"),propertyValue)); + }else if("property.set".equals(change)){ + playOK(tree.currentPathSpeech()); + }else if("property.remove".equals(change)){ + PropertyChangeArgs args = (PropertyChangeArgs)e.getArguments(); + playOK(MessageFormat.format(resources.getString("speech.deleted.property.ack"),args.getOldValue(),tree.currentPathSpeech())); + }else if("property.modifiers".equals(change)){ + playOK(tree.currentPathSpeech()); + }else if("arrowHead".equals(change)||"endLabel".equals(change)){ + playOK(tree.currentPathSpeech()); + } + } + + @Override + public void bookmarkAdded(DiagramTreeNodeEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + playOK(tree.currentPathSpeech()); + } + } + + @Override + public void bookmarkRemoved(DiagramTreeNodeEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + playOK(MessageFormat.format( + resources.getString("speech.delete.bookmark.ack"), + evt.getValue(), + tree.currentPathSpeech())); + } + } + + @Override + public void notesChanged(DiagramTreeNodeEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + playOK(tree.currentPathSpeech()); + } + } + + private void playOK(final String speech){ + SoundFactory.getInstance().play(SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(speech); + } + }); + } + + private ResourceBundle resources; + private DiagramTree tree; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/CCmIPopupMenu.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,442 @@ +/* + 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.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +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.network.AwarenessMessage; +import uk.ac.qmul.eecs.ccmi.network.Command; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * This class provides the two menus to handle nodes and edges on the visual graph. This class + * provides an abstract implementation common to both the node and edge menus. The specific + * implementations are internal static classes, inheriting from this class. + * + */ +@SuppressWarnings("serial") +public abstract class CCmIPopupMenu extends JPopupMenu { + /** + * This constructor is called by subclasses constructor. + * + * @param reference the element this menu refers to (it popped up by right-clicking on it) + * @param parentComponent the component where the menu is going to be displayed + * @param modelUpdater the model updater to make changed to {@code reference} + * @param selectedElements other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code reference}, being selected together with it. + */ + protected CCmIPopupMenu(DiagramElement reference, + Component parentComponent, DiagramModelUpdater modelUpdater, + Set<DiagramElement> selectedElements) { + super(); + this.modelUpdater = modelUpdater; + this.parentComponent = parentComponent; + this.reference = reference; + this.selectedElements = selectedElements; + } + + /** + * Returns the the element this menu refers to. + * @return the element this menu refers to. + */ + public DiagramElement getElement(){ + return reference; + } + + /** + * Add the a menu item to this menu. A menu item, once clicked on, will prompt the user for a new name + * for the referee element and will execute the update through the modelUpdater passed as argument + * to the constructor. + */ + protected void addNameMenuItem() { + /* add set name menu item */ + JMenuItem setNameMenuItem = new JMenuItem( + resources.getString("menu.set_name")); + setNameMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String type = (reference instanceof Node) ? "node" : "edge"; + if (!modelUpdater.getLock(reference, Lock.NAME, + new DiagramEventActionSource(DiagramEventSource.GRPH, + Command.Name.SET_NODE_NAME, reference.getId(),reference.getName()))) { + iLog("Could not get the lock on " + type + " for renaming", + DiagramElement.toLogString(reference)); + JOptionPane.showMessageDialog(parentComponent, + MessageFormat.format(resources + .getString("dialog.lock_failure.name"), + type)); + return; + } + iLog("open rename " + type + " dialog", + DiagramElement.toLogString(reference)); + String name = JOptionPane.showInputDialog(parentComponent, + MessageFormat.format( + resources.getString("dialog.input.name"), + reference.getName()), reference.getName()); + if (name == null) + iLog("cancel rename " + type + " dialog", + DiagramElement.toLogString(reference)); + else + /* node has been locked at selection time */ + modelUpdater.setName(reference, name.trim(), + DiagramEventSource.GRPH); + modelUpdater.yieldLock(reference, Lock.NAME, + new DiagramEventActionSource(DiagramEventSource.GRPH, + Command.Name.SET_NODE_NAME, reference.getId(),reference.getName())); + } + + }); + add(setNameMenuItem); + } + + /** + * Add the a delete item to this menu. A menu item, once clicked on, will prompt the user for a confirmation + * for the deletion of referee element and will execute the update through the modelUpdater passed as argument + * to the constructor. + */ + protected void addDeleteMenuItem() { + JMenuItem deleteMenuItem = new JMenuItem(resources.getString("menu.delete")); + deleteMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + /* create a new Set to maintain iterator consistency as elementTakenOut will change selectedItems */ + List<DiagramElement> workList = new ArrayList<DiagramElement>(selectedElements); + /* right click on an element with no selection involved */ + if(workList.isEmpty()){ + workList.add(reference); + /* right click on an element with other elements selected, thus we ignore the * + * currently selected elements and try to delete only the right clicked one. */ + }else if(!workList.contains(reference)){ + workList.clear(); + workList.add(reference); + }else{ + /* If the right clicked element selected together with other elements, try to * + * delete them all. First delete all edges and then all nodes to keep consistency. * + * We are deleting a bunch of objects and if a node is deleted before an edge * + * attached to it, then an exception will be triggered at the moment of edge * + * deletion because the edge will be already deleted as a result of the node deletion */ + Collections.sort(workList, new Comparator<DiagramElement>(){ + @Override + public int compare(DiagramElement e1,DiagramElement e2) { + boolean e1isEdge = e1 instanceof Edge; + boolean e2isEdge = e2 instanceof Edge; + if(e1isEdge && !e2isEdge){ + return -1; + } + if(!e1isEdge && e2isEdge){ + return 1; + } + return 0; + } + }); + } + + List<DiagramElement>alreadyLockedElements = new ArrayList<DiagramElement>(workList.size()); + /* 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=workList.iterator(); itr.hasNext();){ + DiagramElement selected = itr.next(); + boolean isNode = selected instanceof Node; + if(!modelUpdater.getLock(selected, + Lock.DELETE, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + selected.getId(),selected.getName()))){ + itr.remove(); + alreadyLockedElements.add(selected); + } + } + + /* all the elements are locked by other clients */ + if(workList.isEmpty()){ + iLog("Could not get lock on any selected element for deletion",""); + JOptionPane.showMessageDialog( + JOptionPane.getFrameForComponent(parentComponent), + 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(parentComponent), + 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 : workList){ + modelUpdater.takeOutFromCollection(selected,DiagramEventSource.GRPH); + modelUpdater.sendAwarenessMessage( + AwarenessMessage.Name.STOP_A, + new DiagramEventActionSource(DiagramEventSource.TREE, + (selected instanceof Node) ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + selected.getId(),selected.getName()) + ); + } + }else{ + /* the user chose not to delete the elements, release the acquired locks */ + for(DiagramElement selected : workList){ + /* 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); + }}*/ + boolean isNode = selected instanceof Node; + modelUpdater.yieldLock(selected, + Lock.DELETE, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + selected.getId(),selected.getName())); + } + iLog("cancel delete node dialog",""); + } + } + }); + add(deleteMenuItem); + } + + /** + * Performs the log in the InteractionLog. + * @param action the action to log. + * @param args additional arguments to add to the log. + * + * @see uk.ac.qmul.eecs.ccmi.utils#InteractionLog + */ + protected void iLog(String action, String args) { + InteractionLog.log("GRAPH", action, args); + } + + /** + * + * A popup menu to perform changes (e.g. delete, rename etc.) to a node from the visual graph. + * + */ + public static class NodePopupMenu extends CCmIPopupMenu { + /** + * + * @param node the node this menu refers to. + * @param parentComponent the component where the menu is going to be displayed. + * @param modelUpdater the model updater used to make changed to {@code node}. + * @param selectedElements other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code node}, being selected together with it. + */ + NodePopupMenu(Node node, Component parentComponent, + DiagramModelUpdater modelUpdater, + Set<DiagramElement> selectedElements) { + super(node, parentComponent, modelUpdater, selectedElements); + addNameMenuItem(); + addPropertyMenuItem(); + addDeleteMenuItem(); + } + + private void addPropertyMenuItem() { + final Node nodeRef = (Node) reference; + /* 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, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName()))){ + iLog("Could not get the lock on node for properties",DiagramElement.toLogString(nodeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.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, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName())); + return; + } + if(!properties.isNull()) + modelUpdater.setProperties(nodeRef,properties,DiagramEventSource.GRPH); + modelUpdater.yieldLock(nodeRef, Lock.PROPERTIES,new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName())); + } + }); + add(setPropertiesMenuItem); + } + } + + /** + * A popup menu to perform changes (e.g. delete, rename etc.) to a edge from the visual graph. + */ + public static class EdgePopupMenu extends CCmIPopupMenu { + /** + * Constructs an {@code EdgePopupMenu} to perform changes to an edge from the visual diagram. + * This constructor is normally called when the user clicks in the neighbourhood of a node + * connected to this edge. the menu will then include items to change an end label or + * an arrow head. + * @param edge the edge this menu refers to. + * @param node one attached node some menu item will refer to. + * @param parentComponent the component where the menu is going to be displayed. + * @param modelUpdater the model updater used to make changed to {@code edge}. + * @param selectedElements other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code edge}, being selected together with it. + */ + public EdgePopupMenu( Edge edge, Node node, Component parentComponent, DiagramModelUpdater modelUpdater, + Set<DiagramElement> selectedElements){ + super(edge,parentComponent,modelUpdater,selectedElements); + addNameMenuItem(); + if(node != null){ + nodeRef = node; + Object[] arrowHeads = new Object[edge.getAvailableEndDescriptions().length + 1]; + for(int i=0;i<edge.getAvailableEndDescriptions().length;i++){ + arrowHeads[i] = edge.getAvailableEndDescriptions()[i].toString(); + } + arrowHeads[arrowHeads.length-1] = Edge.NO_ENDDESCRIPTION_STRING; + addEndMenuItems(arrowHeads); + } + addDeleteMenuItem(); + } + + /** + * Constructs an {@code EdgePopupMenu} to perform changes to an edge from the visual diagram. + * This constructor is normally called when the user clicks around the midpoint of the edge + * @param edge the edge this menu refers to. + * @param parentComponent the component where the menu is going to be displayed. + * @param modelUpdater the model updater used to make changed to {@code edge}. + * @param selectedElements other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code edge}, being selected together with it. + */ + public EdgePopupMenu( Edge edge, Component parentComponent, DiagramModelUpdater modelUpdater, + Set<DiagramElement> selectedElements){ + this(edge,null,parentComponent,modelUpdater,selectedElements); + } + + private void addEndMenuItems(final Object[] arrowHeads){ + final Edge edgeRef = (Edge)reference; + /* 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, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDLABEL,edgeRef.getId(),reference.getName()))){ + 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,DiagramEventSource.GRPH); + else + iLog("cancel edge label dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDLABEL,edgeRef.getId(),reference.getName())); + } + }); + 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, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName()))){ + 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, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName())); + return; + } + for(int i=0; i<edgeRef.getAvailableEndDescriptions().length;i++){ + if(edgeRef.getAvailableEndDescriptions()[i].toString().equals(arrowHead)){ + modelUpdater.setEndDescription(edgeRef, nodeRef, i,DiagramEventSource.GRPH); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName())); + return; + } + } + /* the user selected the none menu item */ + modelUpdater.setEndDescription(edgeRef,nodeRef, Edge.NO_END_DESCRIPTION_INDEX,DiagramEventSource.GRPH); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName())); + } + }); + add(selectArrowHeadMenuItem); + } + } + + private Node nodeRef; + } + + /** + * the model updater used to make changed to {@code reference}. + */ + protected DiagramModelUpdater modelUpdater; + /** + * the component where the menu is going to be displayed. + */ + protected Component parentComponent; + /** + * the element this menu refers to. + */ + protected DiagramElement reference; + /** + * other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code reference}, being selected together with it. + */ + protected Set<DiagramElement> selectedElements; + private static ResourceBundle resources = ResourceBundle.getBundle(CCmIPopupMenu.class.getName()); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/CCmIPopupMenu.properties Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,27 @@ + + + +dialog.lock_failure.name={0} 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 + + +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.confirm.deletions=Are you sure you want to delete the selected objects ? +dialog.confirm.title=Confirm + +menu.set_name=Set Name +menu.set_properties=Set Properties +menu.delete=Delete +menu.set_label=Set Label +menu.choose_arrow_head=Set Arrow Head + +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
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/Diagram.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Diagram.java Wed Apr 25 17:09:09 2012 +0100 @@ -24,40 +24,113 @@ 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.DiagramTreeNode; 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; +import uk.ac.qmul.eecs.ccmi.network.AwarenessMessage; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; /** - * This Diagram class holds all the data needed for a representation of the diagram. It is used by component classes + * The {@code 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 { + /** + * Crates a new instance of a Diagram. The diagram created through this method is not shared with any peer via + * a server. + * @param name the name of the diagram. + * @param nodes an array of node prototypes. Nodes inserted by users in the diagram will be created by cloning these nodes. + * @param edges an array of edge prototypes. Edges inserted by users in the diagram will be created by cloning these edges. + * @param prototypePersistenceDelegate a delegate class to handle nodes and edges persistence. + * @return a new instance of {@code Diagram} + */ public static Diagram newInstance(String name, Node[] nodes, Edge[] edges, PrototypePersistenceDelegate prototypePersistenceDelegate){ return new LocalDiagram(name,nodes,edges,prototypePersistenceDelegate); } + /** + * Returns the name of the diagram. The name identifies the diagram uniquely in the editor. There cannot + * be two diagrams with the same name open at the same time. This makes things easier when sharing diagrams + * with other users via the network. + * + * @return the name of the diagram + */ public abstract String getName(); + /** + * Assign this diagram a new name. + * @param name the new name of the diagram + */ public abstract void setName(String name); + /** + * Returns an array with the node prototypes. Node prototypes are used when creating new node + * instances via the {@code clone()} method. + * + * @return an array of nodes + */ public abstract Node[] getNodePrototypes(); + /** + * Returns an array with the edge prototypes. Edge prototypes are used when creating new edge + * instances via the {@code clone()} method. + * + * @return an array of edges + */ public abstract Edge[] getEdgePrototypes(); + /** + * Returns the tree model of this diagram. Note that each diagram holds a {@code DiagramModel} + * which has two sub-models ({@code TreeModel} and {@code CollectionModel}). Changes on one + * sub-model will affect the other model as well. + * + * @return the tree model of this diagram + * + * @see uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel + */ public abstract TreeModel<Node,Edge> getTreeModel(); + /** + * Returns the collection model of this diagram. Note that each diagram holds a {@code DiagramModel} + * which has two sub-models ({@code TreeModel} and {@code CollectionModel}). Changes on one + * sub-model will affect the other model as well. + * + * @return the tree model of this diagram + * + * @see uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel + */ public abstract CollectionModel<Node,Edge> getCollectionModel(); + /** + * Returns the model updater of this diagram. The model updater is the delegate for all the + * update operations affecting the diagram model. + * + * @return the model updater for this diagram + */ public abstract DiagramModelUpdater getModelUpdater(); + /** + * Returns the label of the diagram. The label is slightly different from the name as it's the string + * appearing in the tabbed pane of the editor. It includes asterisk character at the end when the {@code DiagramModel} + * of this class has been changed and not yet saved on hard disk. + * + * @return a label for this diagram + */ public abstract String getLabel(); + /** + * Returns the delegates for this diagram for nodes and edges prototypes persistence. + * When saving a diagram to an xml file each node and edge of the prototypes is encoded + * in the xml file. Indeed the template of a diagram is made of of its prototypes. + * In the template is held the general attributes common to all the nodes and edges, like + * for instance the type of a node but not its current position. + * + * @return the PrototypePersistenceDelegate for this diagram + */ public abstract PrototypePersistenceDelegate getPrototypePersistenceDelegate(); @Override @@ -130,6 +203,9 @@ return prototypePersistenceDelegate; } + /** + * Creates a new {@code Diagram} by clonation. + */ @Override public Object clone(){ LocalDiagram clone = (LocalDiagram)super.clone(); @@ -152,111 +228,114 @@ private class InnerModelUpdater implements DiagramModelUpdater { @Override - public boolean getLock(DiagramModelTreeNode treeNode, Lock lock) { + public boolean getLock(DiagramTreeNode treeNode, Lock lock, DiagramEventActionSource source) { /* 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) {} + public void yieldLock(DiagramTreeNode treeNode, Lock lock, DiagramEventActionSource actionSource) {} + @Override + public void sendAwarenessMessage(AwarenessMessage.Name awMsgName, Object source){} + @Override - public void insertInCollection(DiagramElement element) { + public void insertInCollection(DiagramElement element,DiagramEventSource source) { if(element instanceof Node) - diagramModel.getDiagramCollection().insert((Node)element); + diagramModel.getDiagramCollection().insert((Node)element,source); else - diagramModel.getDiagramCollection().insert((Edge)element); + diagramModel.getDiagramCollection().insert((Edge)element,source); } @Override public void insertInTree(DiagramElement element) { if(element instanceof Node) - diagramModel.getTreeModel().insertTreeNode((Node)element); + diagramModel.getTreeModel().insertTreeNode((Node)element,DiagramEventSource.TREE); else - diagramModel.getTreeModel().insertTreeNode((Edge)element); + diagramModel.getTreeModel().insertTreeNode((Edge)element,DiagramEventSource.TREE); } @Override - public void takeOutFromCollection(DiagramElement element) { - diagramModel.getDiagramCollection().takeOut(element); + public void takeOutFromCollection(DiagramElement element, DiagramEventSource source) { + diagramModel.getDiagramCollection().takeOut(element,source); } @Override public void takeOutFromTree(DiagramElement element) { - diagramModel.getDiagramCollection().takeOut(element); + diagramModel.getTreeModel().takeTreeNodeOut(element,DiagramEventSource.TREE); } @Override - public void setName(DiagramElement element, String name) { - element.setName(name); + public void setName(DiagramElement element, String name,DiagramEventSource source) { + element.setName(name,source); } @Override - public void setNotes(DiagramModelTreeNode treeNode, String notes) { - diagramModel.getTreeModel().setNotes(treeNode, notes); + public void setNotes(DiagramTreeNode treeNode, String notes,DiagramEventSource source) { + diagramModel.getTreeModel().setNotes(treeNode, notes,source); } @Override public void setProperty(Node node, String type, int index, - String value) { - node.setProperty(type, index, value); + String value,DiagramEventSource source) { + node.setProperty(type, index, value,source); } @Override - public void setProperties(Node node, NodeProperties properties) { - node.setProperties(properties); + public void setProperties(Node node, NodeProperties properties,DiagramEventSource source) { + node.setProperties(properties,source); } @Override - public void clearProperties(Node node) { - node.clearProperties(); + public void clearProperties(Node node,DiagramEventSource source) { + node.clearProperties(source); } @Override - public void addProperty(Node node, String type, String value) { - node.addProperty(type, value); + public void addProperty(Node node, String type, String value,DiagramEventSource source) { + node.addProperty(type, value,source); } @Override - public void removeProperty(Node node, String type, int index) { - node.removeProperty(type, index); + public void removeProperty(Node node, String type, int index,DiagramEventSource source) { + node.removeProperty(type, index,source); } @Override public void setModifiers(Node node, String type, int index, - Set<Integer> modifiers) { - node.setModifierIndexes(type, index, modifiers); + Set<Integer> modifiers,DiagramEventSource source) { + node.setModifierIndexes(type, index, modifiers,source); } @Override - public void setEndLabel(Edge edge, Node node, String label) { - edge.setEndLabel(node, label); + public void setEndLabel(Edge edge, Node node, String label,DiagramEventSource source) { + edge.setEndLabel(node, label,source); } @Override public void setEndDescription(Edge edge, Node node, - int index) { - edge.setEndDescription(node, index); + int index,DiagramEventSource source) { + edge.setEndDescription(node, index,source); } @Override - public void translate(GraphElement ge, Point2D p, double x, double y) { - ge.translate(p, x, y); + public void translate(GraphElement ge, Point2D p, double x, double y,DiagramEventSource source) { + ge.translate(p, x, y,source); } @Override - public void startMove(GraphElement ge, Point2D p) { - ge.startMove(p); + public void startMove(GraphElement ge, Point2D p,DiagramEventSource source) { + ge.startMove(p,source); } @Override - public void bend(Edge edge, Point2D p) { - edge.bend(p); + public void bend(Edge edge, Point2D p,DiagramEventSource source) { + edge.bend(p,source); } @Override - public void stopMove(GraphElement ge) { - ge.stopMove(); + public void stopMove(GraphElement ge,DiagramEventSource source) { + ge.stopMove(source); } } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramEventSource.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,148 @@ +/* + 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; + +/** + * This class identifies the source of a diagram event, that is an event generated by an action + * in the diagram such as for instance a node insertion, deletion or renaming. The class carries informations + * about how the event was generated (from the tree, from the graph etc.) and if the event was + * generated by the local user or another user co-editing the diagram. In either case an id of the + * user is conveyed as well. An id is just a String each user assigns to themselves through a user + * interface panel. + */ +public class DiagramEventSource { + /** + * An enumeration of the different ways an event can be generated. NONE is for when the + * information is not relevant (as normally no event listener will be triggered by the event). + * PERS is for actions triggered when rebuilding a diagram from a ccmi file, so not as a consequence + * of a direct user action. + */ + public static enum Type{ + TREE, + GRPH, // graph + HAPT, // haptics + PERS, // persistence + NONE; + } + + /* constructor used only by the static event sources */ + private DiagramEventSource(Type type){ + this.type = type; + local = true; + } + + /** + * Creates a new DiagramEventSource out of a previous one (normally one of the static default sources). + * The type of the new object will be the same as the one passed as argument. Object created through + * this constructor are marked as non local + * + * @see #isLocal() + * @param eventSource an instance of this class + */ + public DiagramEventSource(DiagramEventSource eventSource){ + this.type = eventSource.type; + local = false; + } + + /** + * Returns true if the event is local, that is it's has been generated from an action of + * the local user and not from a message coming from the server. + * + * @return {@code true} if the event has been generated by the local user + */ + public boolean isLocal(){ + return local; + } + + /** + * Returns a copy of this event source that is marked as local. + * + * @return a local copy of this event source + * @see #isLocal() + */ + public DiagramEventSource getLocalSource(){ + return new DiagramEventSource(type); + } + + /** + * Returns the name of the diagram where the event happened, that has this + * object as source. + * + * @return the name of the diagram + */ + public String getDiagramName(){ + return diagramName; + } + + /** + * Sets the name of the diagram where the event happened, that has this + * object as source. + * + * @param diagramName the name of the diagram + */ + public void setDiagramName(String diagramName){ + this.diagramName = diagramName; + } + + /** + * The String representation of this object is the concatenation of the type + * and the ID. + * + * @return a String representing this object + */ + @Override + public String toString(){ + return (local ? ISLOCAL_CHAR : ISNOTLOCAL_CHAR )+type.toString(); + } + + /** + * Returns an instance of this class out of a string representation, such as + * returned by {@code toString()} + * @param s a String representation of a {@code DiagramEventSource} instance, such as + * returned by {@code toString()} + * @return a new instance of {@code DiagramEventSource} + */ + public static DiagramEventSource valueOf(String s){ + Type t = Type.valueOf(s.substring(1, 5)); + DiagramEventSource toReturn = new DiagramEventSource(t); + toReturn.local = (s.charAt(0) == ISLOCAL_CHAR) ? true : false; + return toReturn; + } + + private boolean local; + public final Type type; + private String diagramName; + private static char ISLOCAL_CHAR = 'L'; + private static char ISNOTLOCAL_CHAR = 'R'; + + /** Source for events triggered by the local user through the tree. These static sources + * are used when the diagram is not shared with any other user. When it is, a new DiagramEventSource + * will be created, which includes informations about the id and locality of the user who generated the event + */ + public static DiagramEventSource TREE = new DiagramEventSource(Type.TREE); + /** Source for events triggered by the local user through the graph */ + public static DiagramEventSource GRPH = new DiagramEventSource(Type.GRPH); + /** Source for events triggered by the local user through the haptic device */ + public static DiagramEventSource HAPT = new DiagramEventSource(Type.HAPT); + /** Source for events triggered by the local user when opening a file */ + public static DiagramEventSource PERS = new DiagramEventSource(Type.PERS); + /** Source for events not relevant to model listeners */ + public static DiagramEventSource NONE = new DiagramEventSource(Type.NONE); + +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramModelUpdater.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramModelUpdater.java Wed Apr 25 17:09:09 2012 +0100 @@ -22,8 +22,11 @@ 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.DiagramTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.network.AwarenessMessage; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; +import uk.ac.qmul.eecs.ccmi.network.NetDiagram; /** * @@ -38,43 +41,220 @@ */ public interface DiagramModelUpdater { - public boolean getLock(DiagramModelTreeNode treeNode, Lock lock); + /** + * Issues a lock request to the server for the specified tree node. + * + * @param treeNode the tree node the lock is being requested for + * @param lock the type of lock being requested + * @param actionSource The {@code DiagramEventActionSource} that's going to be piggybacked + * on the lock message, for awareness purposes + * @return {@code true} if the lock is successfully granted by the server + */ + public boolean getLock(DiagramTreeNode treeNode, Lock lock, DiagramEventActionSource actionSource); - public void yieldLock(DiagramModelTreeNode treeNode, Lock lock); + /** + * Releases a lock previously acquired by this client. + * + * @param treeNode the tree node the lock is being released for + * @param lock the type of lock being released + * @param actionSource The {@code DiagramEventActionSource} that's going to be piggybacked + * on the lock message, for awareness purposes. + * + * @see uk.ac.qmul.eecs.ccmi.network.AwarenessMessage + */ + public void yieldLock(DiagramTreeNode treeNode, Lock lock ,DiagramEventActionSource actionSource); - public void insertInCollection(DiagramElement element); + /** + * Sends an awareness message to the server. + * + * @param awMsgName the type of awareness message being sent + * @param source the source of the action. Represents informations to be carried on this message. + * + * @see uk.ac.qmul.eecs.ccmi.network.AwarenessMessage + */ + public void sendAwarenessMessage(AwarenessMessage.Name awMsgName, Object source); + /** + * Inserts a {@code DiagramElement} in the {@code CollectionModel} of the {@code Diagram} holding this + * model updater. + * + * @param element the element to insert + * @param source the source of the insertion action. it can be used by collection listeners. + */ + public void insertInCollection(DiagramElement element,DiagramEventSource source); + + /** + * Inserts a {@code DiagramElement} in the {@code TreeModel} of the {@code Diagram} holding this + * model updater. + * + * @param element the element to insert + */ public void insertInTree(DiagramElement element); - public void takeOutFromCollection(DiagramElement element); + /** + * Removes an element from the {@code CollectionModel} of the {@code Diagram} holding this + * model updater. + * + * @param element the element to remove + * @param source the source of the insertion action. it can be used by collection listeners. + */ + public void takeOutFromCollection(DiagramElement element,DiagramEventSource source); + /** + * Removes an element from the {@code TreeModel} of the {@code Diagram} holding this + * model updater. + * + * @param element the element to remove + */ public void takeOutFromTree(DiagramElement element); - - public void setName(DiagramElement element, String name); - public void setProperty(Node node, String type, int index, String value); + /** + * Sets a new name for the element of the {@code Diagram} holding this + * model updater. + * + * @param element the element being renamed + * @param name the new name + * @param source the source of the removal action. it can be used by collection listeners. + */ + public void setName(DiagramElement element, String name,DiagramEventSource source); - public void setProperties(Node node, NodeProperties properties); + /** + * Sets to a new value a property of a node of the {@code Diagram} holding this + * model updater. + * + * @param node the node being set a new property + * @param type the type of the new property + * @param index the index of the property being set a new value + * @param value the new value for the property + * @param source source the source of the {@code setName} action. it can be used by collection listeners. + */ + public void setProperty(Node node, String type, int index, String value,DiagramEventSource source); - public void clearProperties(Node node); + /** + * Replace the whole {@code NodeProperties} object of a node of the {@code Diagram} holding this + * model updater with a new one. + * + * @param node the node being set a new {@code NodeProperties} instance + * @param properties the new {@code NodeProperties} instance + * @param source source the source of the {@code setProperty} action. it can be used by collection listeners. + */ + public void setProperties(Node node, NodeProperties properties,DiagramEventSource source); - public void setNotes(DiagramModelTreeNode treeNode, String notes); + /** + * Clears the properties of a node of the {@code Diagram} holding this + * model updater. + * + * @param node the node whose properties are being cleared + * @param source the source of the {@code setProperties} action. it can be used by collection listeners. + * + * @see uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties#clear() + */ + public void clearProperties(Node node,DiagramEventSource source); + + /** + * Set the notes for a tree node of the {@code Diagram} holding this + * model updater. + * + * @param treeNode the tree node whose notes are being set + * @param notes the new notes + * @param source the source of the {@code setNotes} action. it can be used by collection listeners. + */ + public void setNotes(DiagramTreeNode treeNode, String notes,DiagramEventSource source); - public void addProperty(Node node, String type, String value); + /** + * Add a new property to a node's properties of the {@code Diagram} holding this + * model updater. + * + * @param node the node whose properties are being added to + * @param type the type of the new property being added + * @param value the value of the new property being added + * @param source the source of the {@code setProperty} action. it can be used by collection listeners. + */ + public void addProperty(Node node, String type, String value,DiagramEventSource source); - public void removeProperty(Node node, String type, int index); + /** + * Removes a property from a node's properties of the {@code Diagram} holding this + * model updater. + * + * @param node the node whose properties are being removed from + * @param type the type of the new property being removed + * @param index the index of the property being removed + * @param source the source of the {@code removeProperty} action. it can be used by collection listeners. + */ + public void removeProperty(Node node, String type, int index,DiagramEventSource source); - public void setModifiers(Node node, String type, int index, Set<Integer> modifiers); + /** + * Set the modifiers for a property of a node in of the {@code Diagram} holding this + * model updater. + * + * @param node the node whose properties whose modifiers are being + * @param type the type of the property whose modifiers are being set + * @param index the index of the property whose modifiers are being set + * @param modifiers the new modifiers indexes. the indexes refer to the modifiers type array. + * @param source the source of the {@code setModifiers} action. it can be used by collection listeners. + */ + public void setModifiers(Node node, String type, int index, Set<Integer> modifiers,DiagramEventSource source); - public void setEndLabel(Edge edge, Node node, String label); + /** + * Set the end label for an edge of the {@code Diagram} holding this + * model updater. + * + * @param edge the edge whose label is being set + * @param node the node at the edge end where the label is being set + * @param label the new label + * @param source the source of the {@code setLabel} action. it can be used by collection listeners. + */ + public void setEndLabel(Edge edge, Node node, String label,DiagramEventSource source); - public void setEndDescription(Edge edge, Node node, int i);// String description); + /** + * Set the end description for an edge of the {@code Diagram} holding this + * model updater. + * + * @param edge the edge whose end description is being set + * @param node the node at the edge end where the end description is being set + * @param index the index of the new end description in the end description array of {@code edge} + * @param source the source of the {@code setEndDescription} action. it can be used by collection listeners. + */ + public void setEndDescription(Edge edge, Node node, int index, DiagramEventSource source); - public void translate(GraphElement ge, Point2D p, double x, double y); + /** + * Translates a graph element of the {@code Diagram} holding this + * model updater. + * + * @param ge the graph element being translated + * @param p the starting point of the translation + * @param x the distance to translate along the x-axis + * @param y the distance to translate along the y-axis + * @param source the source of the {@code translate} action. it can be used by collection listeners. + */ + public void translate(GraphElement ge, Point2D p, double x, double y,DiagramEventSource source); - public void startMove(GraphElement ge, Point2D p); + /** + * Starts the move for a graph element of the {@code Diagram} holding this + * model updater. The move can be either a translation or a bend (if {@code ge} is an Edge). + * This method must be called before such motion methods are called in turn. + * + * @param ge the graph element being translated + * @param p the starting point of the motion + * @param source the source of the {@code startMove} action. it can be used by collection listeners. + */ + public void startMove(GraphElement ge, Point2D p, DiagramEventSource source); - public void bend(Edge edge, Point2D p); + /** + * Bends an edge of the {@code Diagram} holding this model updater. + * + * @param edge the edge being bended + * @param p the starting point of the motion + * @param source the source of the {@code bend} action. it can be used by collection listeners. + */ + public void bend(Edge edge, Point2D p,DiagramEventSource source); - public void stopMove(GraphElement ge); + /** + * Finishes off the motion of a graph element of the {@code Diagram} holding this + * model updater. + * + * @param ge the graph element being moved + * @param sourcethe source of the {@code stopMove} action. it can be used by collection listeners. + */ + public void stopMove(GraphElement ge,DiagramEventSource source); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramPanel.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramPanel.java Wed Apr 25 17:09:09 2012 +0100 @@ -1,6 +1,6 @@ /* 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/) @@ -16,11 +16,12 @@ 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 java.io.IOException; import javax.swing.JPanel; import javax.swing.JScrollPane; @@ -28,105 +29,270 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import uk.ac.qmul.eecs.ccmi.gui.awareness.AwarenessPanel; +import uk.ac.qmul.eecs.ccmi.gui.awareness.DisplayFilter; +import uk.ac.qmul.eecs.ccmi.network.NetDiagram; + /** * It's the panel which displays a diagram. It contains a {@link GraphPanel}, a {@link DiagramTree} - * and a {@link GraphToolbar} - * + * a {@link GraphToolbar} and the {@code AwarenessPanel}. + * It's backed up by an instance of {@code Diagram}. */ @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; - } + /** + * Creates a new instance of {@code DiagramPanel} holding the diagram passed as argument. + * + * @param diagram the diagram this panel is backed up by + * @param tabbedPane the tabbed pane this DiagramPanel will be added to. This reference + * is used to updated the tab label when the diagram is modified or save (in the former + * case a star is added to the label, in the latter case the star is removed) + */ + public DiagramPanel(Diagram diagram, EditorTabbedPane tabbedPane){ + this.diagram = diagram; + this.tabbedPane = tabbedPane; - 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(); - } + setName(diagram.getLabel()); + setLayout(new BorderLayout()); - private Diagram diagram; - private GraphPanel graphPanel; - private DiagramTree tree; - private GraphToolbar toolbar; - private String filePath; - private ChangeListener modelChangeListener; - private EditorTabbedPane tabbedPane; + 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 and the awareness panel */ + visualPanel = new JPanel(new BorderLayout()); + visualPanel.add(toolbar, BorderLayout.NORTH); + visualPanel.add(new JScrollPane(graphPanel),BorderLayout.CENTER); + awarenessSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + awarenessSplitPane.setTopComponent(visualPanel); + + /* divides the tree from the visual diagram */ + JSplitPane treeSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, + new JScrollPane(tree), + awarenessSplitPane); + treeSplitPane.setDividerLocation((int)tree.getPreferredSize().width*2); + add(treeSplitPane, BorderLayout.CENTER); + diagram.getCollectionModel().addChangeListener(modelChangeListener); + } + + /** + * When a diagram is saved on the file system the its path is associated to the diagram panel + * and it's shown when the user hover on the its tab title. + * + * @return the path of the file where this diagram has been saved last time or {@code null} + */ + public String getFilePath(){ + return filePath; + } + + /** + * Sets the file path to a new path. This method should be called after the backing diagram has + * been saved to a file. + * + * @param newValue the path of the file where the backing diagram has been saved last time + */ + public void setFilePath(String newValue){ + filePath = newValue; + } + + /** + * Returns a reference to the backing diagram of this diagram panel. + * + * @return a reference to the backing diagram of this diagram panel + */ + public Diagram getDiagram(){ + return diagram; + } + + /** + * Enables or disables the awareness panel of this diagram panel. As default the awareness panel + * is disabled but if the diagram is shared (either on a local or on a remote server) the awareness + * panel gets enabled. In fact, from now on, awareness messages will be received from the server and, + * even if the awareness panel is not visible, some messages (username messages) + * will still have to be taken into account. + * + * @param enabled {@code true} if the panel is to be enabled, {@code false} otherwise. + */ + public void setAwarenessPanelEnabled(boolean enabled){ + if(!(diagram instanceof NetDiagram)) + return; + /* if the display filter has not been created yet, do create it */ + DisplayFilter filter = DisplayFilter.getInstance(); + if(filter == null) + try{ + filter = DisplayFilter.createInstance(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(this, ioe.getLocalizedMessage()); + return; + } + + NetDiagram netDiagram = (NetDiagram)diagram; + if(enabled){ + awarenessPanel = new AwarenessPanel(diagram.getName()); + awarenessPanelScrollPane = new JScrollPane(awarenessPanel); + netDiagram.enableAwareness(awarenessPanel); + if(awarenessPanelListener != null) + awarenessPanelListener.awarenessPanelEnabled(true); + }else{ //disabled + netDiagram.disableAwareness(awarenessPanel); + if(awarenessSplitPane.getRightComponent() != null){ + // hide the panel + awarenessSplitPane.remove(awarenessPanelScrollPane); + } + awarenessPanelScrollPane = null; + awarenessPanel = null; + awarenessSplitPane.validate(); + if(awarenessPanelListener != null) + awarenessPanelListener.awarenessPanelEnabled(false); + } + } + + /** + * Makes the awareness panel visible or invisible, assuming that it has been enabled beforehand. If the + * awareness panel hasn't been enables this call has no effect. + * + * @param visible {@code true} if the panel is to be made visible, {@code false} otherwise. + */ + public void setAwarenessPanelVisible(boolean visible){ + if(awarenessPanelScrollPane == null) + return; + if(visible){ + awarenessSplitPane.setRightComponent(awarenessPanelScrollPane); + awarenessSplitPane.setDividerLocation(0.8); + awarenessSplitPane.setResizeWeight(1.0); + awarenessSplitPane.validate(); + if(awarenessPanelListener != null) + awarenessPanelListener.awarenessPanelVisible(true); + }else{ + awarenessSplitPane.remove(awarenessPanelScrollPane); + awarenessSplitPane.validate(); + if(awarenessPanelListener != null) + awarenessPanelListener.awarenessPanelVisible(false); + } + } + + /** + * Queries the diagram panel on whether the awareness panel is currently visible. + * + * @return {@code true} if the awareness panel is currently visible, {@code false} otherwise. + */ + public boolean isAwarenessPanelVisible(){ + return (awarenessSplitPane.getRightComponent() != null); + } + + /** + * Returns a reference to the inner awareness panel. + * + * @return the inner awareness panel if it has been enabled of {@code null} otherwise + */ + public AwarenessPanel getAwarenessPanel(){ + return awarenessPanel; + } + + /** + * Sets the backing diagram for this panel. This method is used when a diagram is shared + * (or reverted). A shared diagram has a different way of updating the + * The modified status is changed according to + * the modified status of the {@code DiagramModel} internal to the new {@code Diagram} + * + * @param 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()); + } + + /** + * Returns a reference to the graph panel in this diagram panel. The tree's model is the + * model returned by a calling {@code getTreeModel()} on the backing diagram. + * + * @return the graph panel contained by this diagram panel + */ + public GraphPanel getGraphPanel(){ + return graphPanel; + } + + /** + * Returns a reference to the tree in this diagram panel. The graph model is the + * model returned by a calling {@code getCollectionModel()} on the backing diagram. + * + * @return the tree contained by this diagram panel + */ + public DiagramTree getTree(){ + return tree; + } + + /** + * Changes the {@code modified} status of the backing diagram of this panel. If set to {@code true} + * then a star will appear after the name of the diagram, returned by {@code getName()}. + * + * When called passing false as argument (which should be done after the diagram is saved on a file) + * listeners are notified that the diagram has been saved. + * + * @param modified {@code true} when the diagram has been modified, {@code false} when it 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); + } + + /** + * Whether the backing diagram has been modified. The diagram is modified as a result of changes + * to the {@code TreeModel} or {@code CollectionModel} it contains. To change the {@code modified} + * status of the diagram (and of its models) {@code setModified()} must be used. + * + * @return + */ + public boolean isModified(){ + return diagram.getCollectionModel().isModified(); + } + + void setAwarenessPanelListener(AwarenessPanelEnablingListener listener){ + awarenessPanelListener = listener; + } + + private Diagram diagram; + private GraphPanel graphPanel; + private JSplitPane awarenessSplitPane; + private DiagramTree tree; + private JPanel visualPanel; + private GraphToolbar toolbar; + private AwarenessPanel awarenessPanel; + private JScrollPane awarenessPanelScrollPane; + private String filePath; + private ChangeListener modelChangeListener; + private EditorTabbedPane tabbedPane; + private AwarenessPanelEnablingListener awarenessPanelListener; } + +interface AwarenessPanelEnablingListener { + public void awarenessPanelEnabled(boolean enabled); + public void awarenessPanelVisible(boolean visible); +} +
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramTree.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramTree.java Wed Apr 25 17:09:09 2012 +0100 @@ -40,12 +40,14 @@ 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.DiagramTreeNode; 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.network.Command; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; @@ -54,14 +56,28 @@ import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + @SuppressWarnings("serial") public class DiagramTree extends JTree { + /** + * Creates a new diagram tree. The model of this tree is set to the tree model + * held by the instance of {@code Diagram} passed as argument. The model is retrieved by a call + * to {@code getTreeModel()} on the diagram. + * <p> + * The tree doesn't allow interaction via the mouse. It can be navigated via the keyboard using + * the arrow keys. When a node is selected, cursoring up and down allows the user to go through + * all the sibling of the selected node. Cursoring right will expand the selected node (if it has children) + * and select its first child. Cursoring left will collapse a node and select its father. All the motions + * trigger a text to speech utterance (possibly accompanied by sound) about the new selected node. + * + * @param diagram a reference to the diagram holding the tree model for this tree. + */ public DiagramTree(Diagram diagram){ super(diagram.getTreeModel()); this.diagram = diagram; resources = ResourceBundle.getBundle(EditorFrame.class.getName()); - TreePath rootPath = new TreePath((DiagramModelTreeNode)diagram.getTreeModel().getRoot()); + TreePath rootPath = new TreePath((DiagramTreeNode)diagram.getTreeModel().getRoot()); setSelectionPath(rootPath); collapsePath(rootPath); selectedNodes = new ArrayList<Node>(); @@ -70,7 +86,7 @@ overwriteTreeKeystrokes(); /* don't use the swing focus system as we provide one on our own */ setFocusTraversalKeysEnabled(false); - getAccessibleContext().setAccessibleName(""); + getAccessibleContext().setAccessibleName("tree"); } @SuppressWarnings("unchecked") @@ -79,19 +95,30 @@ return (TreeModel<Node,Edge>)super.getModel(); } + /** + * @see javax.swing.JTree#setModel(javax.swing.tree.TreeModel) + * + * @param newModel the new mnodel for this tree + */ public void setModel(TreeModel<Node,Edge> newModel){ - DiagramModelTreeNode selectedTreeNode = (DiagramModelTreeNode)getSelectionPath().getLastPathComponent(); + DiagramTreeNode selectedTreeNode = (DiagramTreeNode)getSelectionPath().getLastPathComponent(); super.setModel(newModel); collapseRow(0); setSelectionPath(new TreePath(selectedTreeNode.getPath())); } + /** + * Set a new diagram for this tree. As a result of this call the tree model + * of this tree will be set to the model return by {@code diagram.getTreeModel()} + * + * @param diagram the new diagram for this tree + */ public void setDiagram(Diagram diagram){ this.diagram = diagram; setModel(diagram.getTreeModel()); } - public void selectNode(final Node n){ + private void selectNode(final Node n){ selectedNodes.add(n); treeModel.valueForPathChanged(new TreePath(n.getPath()),n.getName()); @@ -104,7 +131,7 @@ InteractionLog.log(INTERACTIONLOG_SOURCE,"node selected for edge",n.getName()); } - public void unselectNode(final Node n){ + private void unselectNode(final Node n){ selectedNodes.remove(n); treeModel.valueForPathChanged(new TreePath(n.getPath()),n.getName()); @@ -117,23 +144,41 @@ InteractionLog.log(INTERACTIONLOG_SOURCE,"node unselected for edge",DiagramElement.toLogString(n)); } + /** + * Returns an array containing the references to all the nodes that have so far been selected + * for edge creation. A new array is created each time this method is called. + * + * @return an array of nodes + */ public DiagramNode[] getSelectedNodes(){ DiagramNode[] array = new DiagramNode[selectedNodes.size()]; return selectedNodes.toArray(array); } + /** + * Makes all the nodes selected for edge creation unselected. This method should + * be called after an edge has been created, to get the user restart + * go over the selection process again. + * + */ 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); + diagram.getModelUpdater().yieldLock(n, Lock.MUST_EXIST, new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.INSERT_EDGE,n.getId(),n.getName())); } } + /** + * Returns a string for a text to speech synthesizer, describing the currently selected + * tree node. The one that is at the end of the current selection path. + * + * @return a description string suitable for text to speech synthesis + */ public String currentPathSpeech(){ TreePath path = getSelectionPath(); - DiagramModelTreeNode selectedPathTreeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + DiagramTreeNode selectedPathTreeNode = (DiagramTreeNode)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()); @@ -142,8 +187,12 @@ } /** - * this method changes the selected tree node from an edge/node reference - * to the related edge/node itself + * Changes the selected tree path from the current to one defined by + * the {@code JumpTo enum} + * + * @see JumpTo + * + * @param jumpTo a {@code JumpTo enum} */ public void jump(JumpTo jumpTo){ final Narrator narrator = NarratorFactory.getInstance(); @@ -151,62 +200,62 @@ switch(jumpTo){ case REFERENCE : oldPath = getSelectionPath(); - DiagramModelTreeNode selectedTreeNode = (DiagramModelTreeNode)oldPath.getLastPathComponent(); + DiagramTreeNode selectedTreeNode = (DiagramTreeNode)oldPath.getLastPathComponent(); if(selectedTreeNode instanceof NodeReferenceMutableTreeNode){ final Node n = (Node)((NodeReferenceMutableTreeNode)selectedTreeNode).getNode(); setSelectionPath(new TreePath(n.getPath())); - SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + SoundFactory.getInstance().play(SoundEvent.JUMP, 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(){ + SoundFactory.getInstance().play(SoundEvent.JUMP,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]); + collapseAll(selectedTreeNode, (DiagramTreeNode)selectedTreeNode.getPath()[1]); break; case ROOT : - final DiagramModelTreeNode from =(DiagramModelTreeNode)getSelectionPath().getLastPathComponent(); + final DiagramTreeNode from =(DiagramTreeNode)getSelectionPath().getLastPathComponent(); setSelectionRow(0); collapseAll(from,from.getRoot()); - SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + SoundFactory.getInstance().play(SoundEvent.JUMP, 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 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 DiagramTreeNode typeTreeNode = (DiagramTreeNode)oldPath.getPathComponent(index); +// setSelectionPath(new TreePath(typeTreeNode.getPath())); +// collapseAll((DiagramTreeNode)oldPath.getLastPathComponent(),typeTreeNode); +// SoundFactory.getInstance().play(SoundEvent.JUMP, new PlayerListener(){ +// @Override +// public void playEnded() { +// narrator.speak(MessageFormat.format(resources.getString("speech.jump"),typeTreeNode.spokenText())); +// } +// }); +// break; case SELECTED_TYPE : - DiagramModelTreeNode root = (DiagramModelTreeNode)getModel().getRoot(); + DiagramTreeNode root = (DiagramTreeNode)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 @@ -242,13 +291,13 @@ } setSelectionPath(new TreePath(typeNode.getPath())); if(oldPath.getPath().length >= 2) - collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(), (DiagramModelTreeNode)initialValue); - SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + collapseAll((DiagramTreeNode)oldPath.getLastPathComponent(), (DiagramTreeNode)initialValue); + SoundFactory.getInstance().play(SoundEvent.JUMP, 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(); @@ -277,16 +326,16 @@ if(bookmark != null){ oldPath = getSelectionPath(); - DiagramModelTreeNode treeNode = treeModel.getBookmarkedTreeNode(bookmark); - collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(), (DiagramModelTreeNode)treeModel.getRoot()); + DiagramTreeNode treeNode = treeModel.getBookmarkedTreeNode(bookmark); + collapseAll((DiagramTreeNode)oldPath.getLastPathComponent(), (DiagramTreeNode)treeModel.getRoot()); setSelectionPath(new TreePath(treeNode.getPath())); final String currentPathSpeech = currentPathSpeech(); - SoundFactory.getInstance().setPlayerListener(new PlayerListener(){ + SoundFactory.getInstance().play(SoundEvent.JUMP, 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 */ @@ -297,13 +346,20 @@ break; } - InteractionLog.log(INTERACTIONLOG_SOURCE,"jumped to "+jumpTo.toString(),((DiagramModelTreeNode)getSelectionPath().getLastPathComponent()).getName()); - SoundFactory.getInstance().play(SoundEvent.JUMP); + InteractionLog.log(INTERACTIONLOG_SOURCE,"jumped to "+jumpTo.toString(),((DiagramTreeNode)getSelectionPath().getLastPathComponent()).getName()); } + /** + * Changes the selected tree path from the current to the one from the root + * to the {@code Diagramelement} passed as argument. Note that a {@code Diagramelement} + * is also an instance of {@code DuagramTreeNode} and it's placed in a {@code TreeModel} + * when it's inserted into a {@code DiagramModel} + * + * @param de the diagram element to be selected on the tree + */ public void jumpTo(final DiagramElement de){ TreePath oldPath = getSelectionPath(); - collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(),de); + collapseAll((DiagramTreeNode)oldPath.getLastPathComponent(),de); setSelectionPath(new TreePath(de.getPath())); SoundFactory.getInstance().play( SoundEvent.JUMP, new PlayerListener(){ @Override @@ -314,26 +370,36 @@ } /* 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; + private void collapseAll(DiagramTreeNode from, DiagramTreeNode to){ + DiagramTreeNode currentNode = from; while(currentNode.getParent() != null && currentNode != to){ currentNode = currentNode.getParent(); collapsePath(new TreePath(currentNode.getPath())); } } + /** + * Mouse events are ignored by this tree. This is just a blank method. + * + * @param e a mouse event + */ @Override protected void processMouseEvent(MouseEvent e){ //do nothing as the tree does not have to be editable with mouse } + /** + * Allows only cursor keys, tab key, delete, and actions (CTRL+something) + * + * @param e a key event + * + */ @Override protected void processKeyEvent(KeyEvent e){ /* allow only cursor keys, tab key, delete, and actions (CTRL+something) */ if(e.getKeyChar() == KeyEvent.CHAR_UNDEFINED || e.getKeyCode() == KeyEvent.VK_TAB || e.getKeyCode() == KeyEvent.VK_SPACE - || e.getKeyCode() == KeyEvent.VK_DELETE || e.isControlDown() || e.isAltDown()) super.processKeyEvent(e); @@ -348,16 +414,16 @@ getActionMap().put("down", new AbstractAction(){ @Override public void actionPerformed(ActionEvent evt) { - DiagramModelTreeNode treeNode = (DiagramModelTreeNode)getLastSelectedPathComponent(); + DiagramTreeNode treeNode = (DiagramTreeNode)getLastSelectedPathComponent(); /* look if we've got a sibling node after (we are not at the bottom) */ - DiagramModelTreeNode nextTreeNode = treeNode.getNextSibling(); + DiagramTreeNode nextTreeNode = treeNode.getNextSibling(); SoundEvent loop = null; if(nextTreeNode == null){ - DiagramModelTreeNode parent = treeNode.getParent(); + DiagramTreeNode 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(); + nextTreeNode = (DiagramTreeNode)parent.getFirstChild(); loop = SoundEvent.LIST_BOTTOM_REACHED; } setSelectionPath(new TreePath(nextTreeNode.getPath())); @@ -376,15 +442,15 @@ getActionMap().put("up", new AbstractAction(){ @Override public void actionPerformed(ActionEvent evt) { - DiagramModelTreeNode treeNode = (DiagramModelTreeNode)getLastSelectedPathComponent(); - DiagramModelTreeNode previousTreeNode = treeNode.getPreviousSibling(); + DiagramTreeNode treeNode = (DiagramTreeNode)getLastSelectedPathComponent(); + DiagramTreeNode previousTreeNode = treeNode.getPreviousSibling(); SoundEvent loop = null; if(previousTreeNode == null){ - DiagramModelTreeNode parent = treeNode.getParent(); + DiagramTreeNode parent = treeNode.getParent(); if(parent == null) /* root node */ previousTreeNode = treeNode; else - previousTreeNode = (DiagramModelTreeNode)parent.getLastChild(); + previousTreeNode = (DiagramTreeNode)parent.getLastChild(); loop = SoundEvent.LIST_TOP_REACHED; } setSelectionPath(new TreePath(previousTreeNode.getPath())); @@ -404,14 +470,14 @@ @Override public void actionPerformed(ActionEvent evt) { TreePath path = getSelectionPath(); - DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + DiagramTreeNode treeNode = (DiagramTreeNode)path.getLastPathComponent(); if(treeNode.isLeaf()){ notifyBorderReached(treeNode); InteractionLog.log(INTERACTIONLOG_SOURCE,"move right","border reached"); } else{ expandPath(path); - setSelectionPath(new TreePath(((DiagramModelTreeNode)treeNode.getFirstChild()).getPath())); + setSelectionPath(new TreePath(((DiagramTreeNode)treeNode.getFirstChild()).getPath())); final String currentPathSpeech = currentPathSpeech(); SoundFactory.getInstance().play(SoundEvent.TREE_NODE_EXPAND,new PlayerListener(){ @Override @@ -419,7 +485,7 @@ NarratorFactory.getInstance().speak(currentPathSpeech); } }); - InteractionLog.log(INTERACTIONLOG_SOURCE,"move right",((DiagramModelTreeNode)treeNode.getFirstChild()).toString()); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move right",((DiagramTreeNode)treeNode.getFirstChild()).toString()); } } }); @@ -429,14 +495,14 @@ @Override public void actionPerformed(ActionEvent evt) { TreePath path = getSelectionPath(); - DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent(); - DiagramModelTreeNode parent = treeNode.getParent(); + DiagramTreeNode treeNode = (DiagramTreeNode)path.getLastPathComponent(); + DiagramTreeNode parent = treeNode.getParent(); if(parent == null){/* root node */ notifyBorderReached(treeNode); InteractionLog.log(INTERACTIONLOG_SOURCE,"move left","border reached"); } else{ - TreePath newPath = new TreePath(((DiagramModelTreeNode)parent).getPath()); + TreePath newPath = new TreePath(((DiagramTreeNode)parent).getPath()); setSelectionPath(newPath); collapsePath(newPath); final String currentPathSpeech = currentPathSpeech(); @@ -446,7 +512,7 @@ NarratorFactory.getInstance().speak(currentPathSpeech); } }); - InteractionLog.log(INTERACTIONLOG_SOURCE,"move left",((DiagramModelTreeNode)parent).toString()); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move left",((DiagramTreeNode)parent).toString()); } } }); @@ -474,7 +540,7 @@ * Narrator.getInstance().speak(builder.toString(), null); */ TreePath path = getSelectionPath(); - DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + DiagramTreeNode treeNode = (DiagramTreeNode)path.getLastPathComponent(); NarratorFactory.getInstance().speak(treeNode.detailedSpokenText()); InteractionLog.log(INTERACTIONLOG_SOURCE,"detailed info requested",""); } @@ -486,14 +552,11 @@ 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)){ + diagram.getModelUpdater().yieldLock(node, Lock.MUST_EXIST,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION,node.getId(),node.getName())); + }else{ + if(!diagram.getModelUpdater().getLock(node, Lock.MUST_EXIST,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SELECT_NODE_FOR_EDGE_CREATION,node.getId(),node.getName()))){ InteractionLog.log(INTERACTIONLOG_SOURCE,"Could not get lock on node fro edge creation selection",DiagramElement.toLogString(node)); SpeechOptionPane.showMessageDialog( SpeechOptionPane.getFrameForComponent(DiagramTree.this), @@ -521,7 +584,7 @@ getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,0),"none"); } - private static InputStream getTreeNodeSound(DiagramModelTreeNode node){ + private static InputStream getTreeNodeSound(DiagramTreeNode node){ InputStream sound = null; TreeNode[] newPath = node.getPath(); if(!node.isRoot()){ @@ -550,7 +613,7 @@ scrollPathToVisible(path); } - private void notifyBorderReached(DiagramModelTreeNode n) { + private void notifyBorderReached(DiagramTreeNode n) { SoundFactory.getInstance().play(SoundEvent.ERROR); } @@ -582,7 +645,30 @@ 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} + /** + * A list of possible destination for a jump (a change of the selected path without + * using the navigation arrow keys) + */ + public static enum JumpTo { + /** + * if the current selection is a edge/node reference tree node, the jump destination + * is the referee tree node (see {@link uk.ac.qmul.eecs.ccmi.diagrammodel.NodeReferenceMutableTreeNode} and + * {@link uk.ac.qmul.eecs.ccmi.diagrammodel.EdgeReferenceMutableTreeNode }) + */ + REFERENCE, + /** + * the destination is the root of the diagram + */ + ROOT, + /** + * the destination will be a node or edge type selected + * (via a selection dialog) by the user + */ + SELECTED_TYPE, + /** + * the destination will be a bookmark selected (via a selection dialog) by the user + */ + 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 @@ -614,14 +700,14 @@ /* 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]; + DiagramTreeNode onPathTreeNode = (DiagramTreeNode)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]; + DefaultMutableTreeNode parent = (DiagramTreeNode)pathArray[i-1]; if(parent.isNodeRelated(root) && parent.getChildCount() > 0){ super.treeStructureChanged(e); setSelectionPath(new TreePath(((DefaultMutableTreeNode)parent.getLastChild()).getPath())); @@ -645,7 +731,7 @@ 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]; + DiagramTreeNode removedTreeNode = (DiagramTreeNode)e.getChildren()[0]; boolean isInSelectionPath = false; for(Object t : getSelectionPath().getPath()){ if(removedTreeNode == t){ @@ -653,7 +739,7 @@ break; } } - DiagramModelTreeNode parentTreeNode = (DiagramModelTreeNode)path.getLastPathComponent(); + DiagramTreeNode parentTreeNode = (DiagramTreeNode)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){ @@ -670,7 +756,7 @@ * 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( + setSelectionPath(new TreePath(((DiagramTreeNode)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())); @@ -678,7 +764,7 @@ /* 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]; + DiagramTreeNode itr = (DiagramTreeNode)pathArray[i]; if(itr.getPath()[0] == getModel().getRoot()){ TreePath newPath = new TreePath(itr.getPath()); setSelectionPath(newPath); @@ -692,7 +778,7 @@ super.treeNodesRemoved(e); /* if the node was selected for edge creation, then remove it from the list */ - DiagramModelTreeNode removedNode = (DiagramModelTreeNode)e.getChildren()[0]; + DiagramTreeNode removedNode = (DiagramTreeNode)e.getChildren()[0]; selectedNodes.remove(removedNode); } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/Direction.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Direction.java Wed Apr 25 17:09:09 2012 +0100 @@ -56,6 +56,12 @@ q.getY() - p.getY()); } + /** + * Checks whether the direction passed as argument is parallel to this direction. + * + * @param d the direction to check against + * @return {@code true} if this direction and {@code d} are parallel to each other, false otherwise + */ public boolean isParallel(Direction d){ if(equals(d.x,d.y,DELTA)||turn(180).equals(d.x,d.y,DELTA)) return true;
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java Wed Apr 25 17:09:09 2012 +0100 @@ -1,6 +1,6 @@ /* 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/) @@ -16,7 +16,7 @@ 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; @@ -57,6 +57,18 @@ @SuppressWarnings("serial") public abstract class Edge extends DiagramEdge implements GraphElement{ + /** + * Creates a new Edge + * + * @param type the type of the edge. The type is just a string that the user assign to the edge. + * All the edges created via clonation from this edge will have the same type. + * @param availableEndDescriptions all the possible end description ends of this edge can be + * associated to. An end description is a text associated to a possible arrow head of an edge end. + * It's used to give awareness of arrow heads via speech. + * @param minAttachedNodes the minimum number of nodes that can be attached to this edge + * @param maxAttachedNodes the minimum number of nodes that can be attached to this edge + * @param style the style of the edge: whether it's solid, dotted or dashed + */ public Edge(String type, String[] availableEndDescriptions, int minAttachedNodes, int maxAttachedNodes,LineStyle style){ super(type,availableEndDescriptions); this.minAttachedNodes = minAttachedNodes; @@ -64,47 +76,175 @@ this.style = style; nodes = new ArrayList<Node>(); } - + + /* --- Methods inherited from DiagramEdge --- */ @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); - } - } + public Node getNodeAt(int index){ + return nodes.get(index); + } - /** - * 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()){ + @Override + public int getNodesNum(){ + return nodes.size(); + } + + @Override + 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); + } + } + + @Override + 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(),DiagramEventSource.NONE); + p.neighbours.addAll(nodeSet); + points.add(p); + } + this.nodes.addAll(nodeSet); + + if(!points.isEmpty())// points is empty when the edge has two nodes only + masterInnerPoint = points.get(0); + } + + @Override + public abstract void draw(Graphics2D g2); + + @Override + public void translate(Point2D p, double dx, double dy, Object source){ + for(InnerPoint ip : points) + ip.translate(p, dx, dy,source); + } + + /** + * To be called before {@code bend}, determines from {@code downPoint} whether + * a line is going to be break into two lines (with the creation of a new inner point) + * or if the bending is determined by an already existing inner point being translated + */ + @Override + public void startMove(Point2D downPoint,Object source){ + 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(),DiagramEventSource.NONE); + newPointCreated = true; + /* this methods checks for segments of the edge which are aligned and makes a unique edge out of them */ + } + } + + /** + * If this edge is made out of several lines and two or more of them becomes + * aligned then they are blended in one single line and the inner point that was + * at the joint is removed. + * + * @param source the source of the {@code stopMove} action + */ + @Override + public void stopMove(Object source){ + 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",source)); + } + + @Override + public abstract Rectangle2D getBounds(); + + @Override + public Point2D getConnectionPoint(Direction d){return null;} + + @Override + 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; + } + + /** + * 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); @@ -129,272 +269,200 @@ } 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); + private void removePoint(InnerPoint p){ + /* we assume at this moment p has one neighbour only */ + InnerPoint neighbour = (InnerPoint)p.neighbours.get(0); + points.remove(p); + neighbour.neighbours.remove(p); + if(neighbour.neighbours.size() == 1) + removePoint(neighbour); + } - /* 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(){ + /* checks if a point belongs to a shape with a margin of MAX_DIST*/ + private boolean fatStrokeContains(Shape s, Point2D p){ + BasicStroke fatStroke = new BasicStroke((float) (2 * MAX_DIST)); + Shape fatPath = fatStroke.createStrokedShape(s); + return fatPath.contains(p); + } + + /** + * Returns a line connecting the centre of the graph elements passed as argument. + * + * @param start the first graph element + * @param end the second graph element + * + * @return a line connecting {@code start} and {@code end} + */ + 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))); + } + + /** + * Returns a list of the points where this edge and the nodes it connects come to a contact + * + * @return a list of points + */ + 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; + } + + /** + * Gets the stipple pattern of this edge line style + * + * @see LineStyle#getStipplePattern() + * + * @return an int representing the stipple pattern of this edge + */ + public int getStipplePattern(){ + return getStyle().getStipplePattern(); + } + + /** + * Bends one of the lines forming this edge. + * + * When an line is bent, if the location where the bending happens is a line then a new + * inner point is created and the line is broken into two sub lines. If the location is an + * already existing inner point, then the point is translated. + * + * @param p + * @param source + */ + public void bend(Point2D p,Object source) { + 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(),DiagramEventSource.NONE); + notifyChange(new ElementChangedEvent(this,this,"bend",source)); + } + + /** + * Returns the line where the edge name will be painted. If the edge is only made out + * of a straight line then this will be returned. + * + * If the edge has been broken into several segments (by bending it) then the central + * line is returned. If the edge connects more than two nodes then a line (not necessarily + * matching the edge) that has the central point in its centre is returned. Note that a + * edge connecting more than two nodes is painted as a central point connected to all the nodes. + * + * @return the line where the name will be painted + */ + 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); + 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); + 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(); - + + /** + * Encodes all the relevant data of this object in XML format. + * + * @param doc an XML document + * @param parent the parent XML element, where tag about this edge will be inserted + * @param nodes a list of all nodes of the diagram + */ public void encode(Document doc, Element parent, List<Node> nodes){ parent.setAttribute(PersistenceManager.TYPE,getType()); parent.setAttribute(PersistenceManager.NAME, getName()); @@ -411,7 +479,7 @@ nodesTag.appendChild(nodeTag); } } - + if(!points.isEmpty()){ Element pointsTag = doc.createElement(PersistenceManager.POINTS); parent.appendChild(pointsTag); @@ -419,13 +487,13 @@ 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(); @@ -442,42 +510,52 @@ } } } - - - 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 { + + + /** + * Decodes an edge from the XML representation. + * + * @see #encode(Document, Element, List) + * + * @param doc an XML document + * @param edgeTag the tag in the XML file related to this edge + * @param nodesId a map linking node ids in the XML file to the {@code Node} objects they represent + * @throws IOException if something goes wrong while reading the XML file + */ + public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{ + setName(edgeTag.getAttribute(PersistenceManager.NAME),DiagramEventSource.PERS); + 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)); + setEndLabel(attachedNodes.get(i), labels.get(i),DiagramEventSource.PERS); } - + 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); @@ -489,9 +567,9 @@ }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); @@ -502,9 +580,9 @@ }catch(NumberFormatException nfe){ throw new IOException(); } - point.translate(new Point2D.Double(), dx, dy); + point.translate(new Point2D.Double(), dx, dy,DiagramEventSource.PERS); } - + /* remove the master inner point eventually created by connect */ /* we're going to replace it with the one in the XML file */ points.clear(); @@ -512,13 +590,13 @@ 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 @@ -528,56 +606,89 @@ point.neighbours.add(ge); } points.add(point); + if(i==0) + masterInnerPoint = point; } - } - - @Override - public Point2D getConnectionPoint(Direction d){return null;} - + } + + /** + * Returns the minimum number of nodes that edge of this type can connect + * + * @return the minimum nodes for edges of this type + */ public int getMinAttachedNodes(){ return minAttachedNodes; } - + + /** + * Returns the maximum number of nodes that edge of this type can connect + * + * @return the maximum nodes for edges of this type + */ public int getMaxAttachedNodes(){ return maxAttachedNodes; } - + + /** + * Return the line style of this edge + * + * @return the line style of this edge + */ public LineStyle getStyle(){ return style; } - + protected Point2D downPoint; private List<Node> nodes; + + /* list containing the vertex of the edge which are not nodes */ + /** + * The list of the inner points of this edge + */ + protected List<InnerPoint> points; + /** + * For edges connecting more than two nodes, this is the central inner point where + * all the lines from the nodes join + */ + protected InnerPoint masterInnerPoint; - /* 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 boolean newPointCreated; + private InnerPoint newInnerPoint; 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"); + /** + * The end description for an end that has no hand description set by the user + */ + public static final String NO_ENDDESCRIPTION_STRING = ResourceBundle.getBundle(EditorFrame.class.getName()).getString("no_arrow_string"); - + + /** + * When an edge's (straight) line is bent it breaks into two sub lines. At the point where this two + * sub lines join a square shaped point is painted. This class represent that point. Objects of this class + * are graph elements and the user can click on them and translate them along the graph. + * + */ protected static class InnerPoint implements GraphElement{ - + /** + * Creates a new inner point + */ public InnerPoint(){ bounds = new Rectangle2D.Double(0,0,DIM,DIM); neighbours = new LinkedList<GraphElement>(); } - + @Override - public void startMove(Point2D p){} - + public void startMove(Point2D p, Object source){} + @Override - public void stopMove(){} - + public void stopMove(Object source){} + @Override public void draw(Graphics2D g2){ Color oldColor = g2.getColor(); @@ -595,38 +706,62 @@ public Point2D getConnectionPoint(Direction d){ return new Point2D.Double(bounds.getCenterX(),bounds.getCenterY()); } - + @Override - public void translate(Point2D p, double dx, double dy){ + public void translate(Point2D p, double dx, double dy, Object source){ bounds.setFrame(bounds.getX() + dx, - bounds.getY() + dy, - bounds.getWidth(), - bounds.getHeight()); + bounds.getY() + dy, + bounds.getWidth(), + bounds.getHeight()); } - + @Override public Rectangle2D getBounds(){ return (Rectangle2D)bounds.clone(); } - + + /** + * Neighbours are the graph elements (either nodes or other inner points) this inner point is + * directly connected to. Directly connected means there is a straight line from this node + * and the neighbour. + * + * @return a list of neighbours of this inner node + */ public List<GraphElement> getNeighbours(){ return neighbours; } - - public boolean hasNeighbour(GraphElement neighbour){ - return neighbours.contains(neighbour); + + /** + * Returns true if this inner node and {@code ge} are neighbours. + * + * @see #getNeighbours() + * + * @param ge the graph element to be tested + * @return {@code true} if {@code ge} is a neighbour of this graph element, {@code false} otherwise + * + */ + public boolean hasNeighbour(GraphElement ge){ + return neighbours.contains(ge); } - + @Override public String toString(){ return "EdgePoint: "+bounds.getCenterX()+"-"+bounds.getCenterY(); } - + private Rectangle2D bounds; private List<GraphElement> neighbours; private static final int DIM = 5; } - + + /** + * A representation of this edge as a set of 2D points. + * Every node the edge connects and every inner point are represented as a pair with coordinates + * of their centre. Furthermore an adjacency matrix holds the information + * about which point is connected to which is. This representation of the edge is + * used in the haptic space, being more suitable for haptic devices. + * + */ public static class PointRepresentation { public PointRepresentation(int size){ xs = new double[size]; @@ -636,12 +771,36 @@ adjMatrix[i] = new BitSet(size); } } + /** + * An array with all the x coordinate of the edge's points (nodes and inner points) + */ public double xs[]; + /** + * An array with all the y coordinate of the edge's points (nodes and inner points) + */ public double ys[]; + /** + * The adjacency matrix. If the i-th bit of {@code adjMatrix[j]} is set to {@code true} + * it means there is a direct line connecting the i-th point (coordinates {@code (xs[i],ys[i])} + * to the j-th point (coordinates {@code (xs[j],ys[j])}. Note that connection are represented only + * once to avoid double paintings by the haptic engine. So if the i-th bit of {@code adjMatrix[j]} + * is set to {@code true}, the j-th bit of {@code adjMatrix[i]} and information redundancy is avoided. + */ public BitSet adjMatrix[]; - public int nodeStart; // the index of the beginning of the nodes in the adjMatrix + /** + * The index of the beginning of the nodes (after inner points) in {@code adjMatrix}. + * So if the edge has three nodes and two inner points. The inner points + * will be at {@code adjMatrix[0]} and {@code adjMatrix[1]} and the nodes at the following indexes. + * In this case {@code nodeStart} is equal to 2. + */ + public int nodeStart; } - + + /** + * Returns a new {@code PointRepresentation} of this edge + * + * @return a new {@code PointRepresentation} of this edge + */ public PointRepresentation getPointRepresentation(){ PointRepresentation pr = new PointRepresentation(points.size()+nodes.size()); if(points.isEmpty()){ // two ended edge @@ -657,14 +816,14 @@ 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)); - } + 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++){
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/EdgePopupMenu.java Mon Feb 06 12:54:06 2012 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,172 +0,0 @@ -/* - 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.trim()); - 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()); -}
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/EdgePopupMenu.properties Mon Feb 06 12:54:06 2012 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ - - -menu.set_label=Set Label -menu.choose_arrow_head=Set 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
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.java Wed Apr 25 17:09:09 2012 +0100 @@ -1,6 +1,6 @@ /* 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/) @@ -16,7 +16,7 @@ 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; @@ -75,19 +75,25 @@ 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.DiagramTreeNode; 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.awareness.BroadcastFilter; +import uk.ac.qmul.eecs.ccmi.gui.awareness.DisplayFilter; 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.AwarenessMessage; import uk.ac.qmul.eecs.ccmi.network.ClientConnectionManager; +import uk.ac.qmul.eecs.ccmi.network.ClientConnectionManager.RmDiagramRequest; +import uk.ac.qmul.eecs.ccmi.network.ClientConnectionManager.SendAwarenessRequest; import uk.ac.qmul.eecs.ccmi.network.Command; import uk.ac.qmul.eecs.ccmi.network.DiagramDownloader; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; import uk.ac.qmul.eecs.ccmi.network.DiagramShareException; import uk.ac.qmul.eecs.ccmi.network.NetDiagram; import uk.ac.qmul.eecs.ccmi.network.ProtocolFactory; @@ -103,1788 +109,2005 @@ 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. -*/ + * 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); + /** + * Creates a new {@code EditorFrame} + * + * @param haptics an instance of {@code Haptics} handling the haptic device during this + * @param templateFiles an array of template files. New diagrams can be created from template files by clonation + * @param backupDirPath the path of a folder where all the currently open diagrams will be saved if + * the haptic device crashes + * @param templateEditors + */ + public EditorFrame(Haptics haptics, File[] templateFiles, String backupDirPath, TemplateEditor[] templateEditors){ + this.backupDirPath = backupDirPath; + /* load resources */ + resources = ResourceBundle.getBundle(this.getClass().getName()); - 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(); + /* haptics */ + this.haptics = haptics; + hapticTrigger = new HapticTrigger(); - int screenWidth = (int)screenSize.getWidth(); - int screenHeight = (int)screenSize.getHeight(); + /* read editor related preferences */ + preferences = PreferencesService.getInstance(); - setLocation(screenWidth / 16, screenHeight / 16); - editorTabbedPane.setPreferredSize(new Dimension( - screenWidth * 5 / 8, screenHeight * 5 / 8)); + setIconImage(new ResourceFactory.ImageFactory().getImage("ccmi_favicon.gif")); + changeLookAndFeel(preferences.get("laf", null)); - /* 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(); - } - }); + 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); - addWindowFocusListener(new WindowFocusListener(){ - @Override - public void windowGainedFocus(WindowEvent evt) { - if(evt.getOppositeWindow() == null) - NarratorFactory.getInstance().speak(resources.getString("window.focus")); - } + /* 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")); - @Override - public void windowLostFocus(WindowEvent evt) { - if(evt.getOppositeWindow() == null) - NarratorFactory.getInstance().speak(resources.getString("window.unfocus")); - } - }); + /* start building the GUI */ + editorTabbedPane = new EditorTabbedPane(this); + setContentPane(editorTabbedPane); - /* 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); + setTitle(resources.getString("app.name")); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - 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")); + 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().setDefaultPlayerListener(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"),"")); } - }); - } - }; - } - - private void initMenu(){ - ResourceFactory factory = new ResourceFactory(resources); + } + }, SoundEvent.CANCEL); + SoundFactory.getInstance().setDefaultPlayerListener(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().setDefaultPlayerListener(new PlayerListener(){ + @Override + public void playEnded() { + DiagramPanel dPanel = getActiveTab(); + if(dPanel != null){ + DiagramTree tree = dPanel.getTree(); + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + } + }, SoundEvent.MESSAGE_OK); - JMenuBar menuBar = factory.createMenuBar(); - setJMenuBar(menuBar); - - /* --- FILE MENU --- */ - JMenu fileMenu = factory.createMenu("file"); - menuBar.add(fileMenu); + /* 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); + } + } - /* menu items and listener added by addDiagramType function */ - newMenu = factory.createMenu("file.new"); - fileMenu.add(newMenu); + 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(); + } + }); - JMenuItem fileOpenItem = factory.createMenuItem( - "file.open", this, "openFile"); - fileMenu.add(fileOpenItem); + addWindowFocusListener(new WindowFocusListener(){ + @Override + public void windowGainedFocus(WindowEvent evt) { + if(evt.getOppositeWindow() == null) + NarratorFactory.getInstance().speak(resources.getString("window.focus")); + } - recentFilesMenu = factory.createMenu("file.recent"); - buildRecentFilesMenu(); - fileMenu.add(recentFilesMenu); + @Override + public void windowLostFocus(WindowEvent evt) { + if(evt.getOppositeWindow() == null) + NarratorFactory.getInstance().speak(resources.getString("window.unfocus")); + } + }); - fileSaveItem = factory.createMenuItem("file.save", this, "saveFile"); - fileMenu.add(fileSaveItem); - - fileSaveAsItem = factory.createMenuItem("file.save_as", this, "saveFileAs"); - fileMenu.add(fileSaveAsItem); + /* 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, otherwise 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()))); - 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); + // updated the haptics + HapticsFactory.getInstance().switchDiagram(diagramPanel.getDiagram().getName()); + 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); - JMenu editMenu = factory.createMenu("edit"); - menuBar.add(editMenu); + treeSelectionListener = new TreeSelectionListener(){ + @Override + public void valueChanged(TreeSelectionEvent evt) { + treeEnabledMenuUpdate(evt.getPath()); + } + }; - 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); + 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")); + } + }); + } + }; + } - insertMenuItem = factory.createMenuItem("edit.insert", this, "insert"); - editMenu.add(insertMenuItem); + private void initMenu(){ + ResourceFactory factory = new ResourceFactory(resources); - 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); + JMenuBar menuBar = factory.createMenuBar(); + setJMenuBar(menuBar); - /* --- VIEW MENU --- */ - JMenu viewMenu = factory.createMenu("view"); - menuBar.add(viewMenu); + /* --- FILE MENU --- */ + JMenu fileMenu = factory.createMenu("file"); + menuBar.add(fileMenu); - 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); - } - })); + /* menu items and listener added by addDiagramType function */ + newMenu = factory.createMenu("file.new"); + fileMenu.add(newMenu); - 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); - } - })); + JMenuItem fileOpenItem = factory.createMenuItem( + "file.open", this, "openFile"); + fileMenu.add(fileOpenItem); - 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(); - } - })); + recentFilesMenu = factory.createMenu("file.recent"); + buildRecentFilesMenu(); + fileMenu.add(recentFilesMenu); - 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(); - } - })); + fileSaveItem = factory.createMenuItem("file.save", this, "saveFile"); + fileMenu.add(fileSaveItem); - 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); - } - })); + fileSaveAsItem = factory.createMenuItem("file.save_as", this, "saveFileAs"); + fileMenu.add(fileSaveAsItem); - 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); - } - })); + fileCloseItem = factory.createMenuItem("file.close",this,"closeFile"); + fileMenu.add(fileCloseItem); - 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()); - } - })); + graphExportItem = factory.createMenuItem("file.export_image", this, "exportImage"); + fileMenu.add(graphExportItem); - 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){} - }); + JMenuItem fileExitItem = factory.createMenuItem( + "file.exit", this, "exit"); + fileMenu.add(fileExitItem); - JMenu lafMenu = factory.createMenu("view.change_laf"); - viewMenu.add(lafMenu); + /* --- EDIT MENU --- */ + JMenu editMenu = factory.createMenu("edit"); + menuBar.add(editMenu); - 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); - SoundFactory.getInstance().play(SoundEvent.OK,null); - } - }) - ); - - 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); - SoundFactory.getInstance().play(SoundEvent.OK,null); - } - } - )); - } - - /* --- 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); + locateMenuItem = factory.createMenuItem("edit.locate", this,"locate"); + editMenu.add(locateMenuItem); - helpMenu.add(factory.createMenuItem( - "help.about", this, "showAboutDialog")); + highlightMenuItem = factory.createMenuItem("edit.highlight", this, "hHighlight"); + highlightMenuItem.setEnabled(false); + editMenu.add(highlightMenuItem); - 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 { + 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); + + selectMenuItem = factory.createMenuItem("edit.select", this, "selectNode"); + editMenu.add(selectMenuItem); + + 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); + SoundFactory.getInstance().play(SoundEvent.OK,null); + } + }) + ); + + 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); + SoundFactory.getInstance().play(SoundEvent.OK,null); + } + } + )); + } + + /* --- COLLABORATION ---- */ + JMenu collabMenu = factory.createMenu("collab"); + menuBar.add(collabMenu); + + startServerMenuItem = factory.createMenuItem("collab.start_server", this, "startServer"); + collabMenu.add(startServerMenuItem); + + stopServerMenuItem = factory.createMenuItem("collab.stop_server", this, "stopServer"); + collabMenu.add(stopServerMenuItem); + stopServerMenuItem.setEnabled(false); + + shareDiagramMenuItem = factory.createMenuItem("collab.share_diagram", this, "shareDiagram"); + collabMenu.add(shareDiagramMenuItem); + + collabMenu.add(factory.createMenuItem("collab.open_shared_diagram", this, "openSharedDiagram")); + + showAwarenessPanelMenuItem = factory.createMenuItem("collab.show_awareness_panel", this, "showAwarenessPanel"); + collabMenu.add(showAwarenessPanelMenuItem); + + hideAwarenessPanelMenuItem = factory.createMenuItem("collab.hide_awareness_panel", this, "hideAwarenessPanel"); + collabMenu.add(hideAwarenessPanelMenuItem); + + awarenessPanelListener = new AwarenessPanelEnablingListener(){ + @Override + public void awarenessPanelEnabled(boolean enabled) { + if(enabled){ + showAwarenessPanelMenuItem.setEnabled(true); + hideAwarenessPanelMenuItem.setEnabled(false); + }else{ + showAwarenessPanelMenuItem.setEnabled(false); + hideAwarenessPanelMenuItem.setEnabled(false); + } + } + + @Override + public void awarenessPanelVisible(boolean visible) { + if(visible){ + showAwarenessPanelMenuItem.setEnabled(false); + hideAwarenessPanelMenuItem.setEnabled(true); + }else{ + showAwarenessPanelMenuItem.setEnabled(true); + hideAwarenessPanelMenuItem.setEnabled(false); + } + } + }; + + /* --- PREFERENCES --- */ + JMenu preferencesMenu = factory.createMenu("preferences"); + menuBar.add(preferencesMenu); + + // awareness menu + JMenu awarenessMenu = factory.createMenu("preferences.awareness"); + preferencesMenu.add(awarenessMenu); + + awarenessMenu.add(factory.createMenuItem("preferences.awareness.username", this, "showAwarnessUsernameDialog")); + awarenessMenu.add(factory.createMenuItem("preferences.awareness.broadcast", this, "showAwarenessBroadcastDialog")); + awarenessMenu.add(factory.createMenuItem("preferences.awareness.display", this, "showAwarenessDisplayDialog")); + + JMenuItem enableAwarenessVoiceMenuItem = factory.createCheckBoxMenuItem("preferences.awareness.enable_voice",new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + JCheckBoxMenuItem menuItem = (JCheckBoxMenuItem) evt.getSource(); + NarratorFactory.getInstance().setMuted(!menuItem.isSelected(),Narrator.SECOND_VOICE); + PreferencesService.getInstance().put("second_voice_enabled", Boolean.toString(menuItem.isSelected())); + } + }); + NarratorFactory.getInstance().setMuted(true,Narrator.FIRST_VOICE); + enableAwarenessVoiceMenuItem.setSelected( + Boolean.parseBoolean(PreferencesService.getInstance().get("second_voice_enabled", Boolean.toString(true)))); + awarenessMenu.add(enableAwarenessVoiceMenuItem); + NarratorFactory.getInstance().setMuted(false,Narrator.FIRST_VOICE); + + // sound + JMenu soundMenu = factory.createMenu("preferences.sound"); + preferencesMenu.add(soundMenu); + + JMenuItem muteMenuItem = factory.createCheckBoxMenuItem("preferences.sound.mute", new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + JCheckBoxMenuItem menuItem = (JCheckBoxMenuItem) evt.getSource(); + NarratorFactory.getInstance().setMuted(menuItem.isSelected(),Narrator.FIRST_VOICE); + SoundFactory.getInstance().setMuted(menuItem.isSelected()); + } + }); + soundMenu.add(muteMenuItem); + + JMenuItem rateMenuItem = factory.createMenuItem("preferences.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.feedback.speech_rate"), + newRate)); + }else{ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + } + }); + soundMenu.add(rateMenuItem); + + //server ports + JMenu networkingMenu = factory.createMenu("preferences.server"); + preferencesMenu.add(networkingMenu); + + JMenuItem localPortMenuItem = factory.createMenuItem("preferences.local_server.port", this, "showLocalServerPortDialog"); + networkingMenu.add(localPortMenuItem); + + JMenuItem remotePortMenuItem = factory.createMenuItem("preferences.remote_server.port", this, "showRemoteServerPortDialog"); + networkingMenu.add(remotePortMenuItem); + + // accessible file chooser + JMenuItem fileChooserMenuItem = factory.createCheckBoxMenuItem("preferences.file_chooser", new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + JCheckBoxMenuItem menuItem = (JCheckBoxMenuItem) evt.getSource(); + PreferencesService.getInstance().put("use_accessible_filechooser", Boolean.toString(menuItem.isSelected())); + } + }); + + // temporarily mute the narrator to select the menu without triggering a speech + NarratorFactory.getInstance().setMuted(true,Narrator.FIRST_VOICE); + fileChooserMenuItem.setSelected(Boolean.parseBoolean(PreferencesService.getInstance().get("use_accessible_filechooser", "true"))); + NarratorFactory.getInstance().setMuted(false,Narrator.FIRST_VOICE); + preferencesMenu.add(fileChooserMenuItem); + + + /* --- 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){ + if(lafName == null) + lafName = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"; + 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); + 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()); + 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 */ } - } - } - - /** + */ + 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())); + if(dPanel.getDiagram() instanceof NetDiagram){ + if(clientConnectionManager != null && clientConnectionManager.isAlive()) + clientConnectionManager.addRequest(new RmDiagramRequest(dPanel.getDiagram().getName())); + } + editorTabbedPane.remove(dPanel); + //getActiveTab, after removing, returns the new selected panel after the remotion, if any + String newFocusedTabName = null; + if(getActiveTab() != null){ + newFocusedTabName = getActiveTab().getDiagram().getName(); + } + haptics.removeDiagram(dPanel.getDiagram().getName(), newFocusedTabName); + } + + 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; + */ + public boolean saveFileAs() { + DiagramPanel diagramPanel = getActiveTab(); + if (diagramPanel == null) // no tabs open + return false; + OutputStream out = null; + try { + String[] currentTabs = new String[editorTabbedPane.getTabCount()]; + for(int i=0; i<editorTabbedPane.getTabCount();i++){ + currentTabs[i] = editorTabbedPane.getTitleAt(i); + } + FileService.Save save = fileService.save( + PreferencesService.getInstance().get("dir.diagrams", "."), + diagramPanel.getFilePath(), + extensionFilter, + null, + defaultExtension, + currentTabs); + 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();} - } - } + 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++; - } - } + */ + 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; - } - } + 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(); + NarratorFactory.getInstance().dispose(); + SoundFactory.getInstance().dispose(); + haptics.dispose(); + if(server != null) + server.shutdown(); + if(clientConnectionManager != null) + clientConnectionManager.shutdown(); + BroadcastFilter broadcastFilter = BroadcastFilter.getInstance(); + if(broadcastFilter != null) + broadcastFilter.saveProperties(this); + DisplayFilter displayFilter = DisplayFilter.getInstance(); + if(displayFilter != null) + displayFilter.saveProperties(this); - 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(){ + 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() { + } + + public void hHighlight() { 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); + public void hPickUp(DiagramElement de) { + HapticsFactory.getInstance().pickUp(System.identityHashCode(de)); + } - 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().play(SoundEvent.ERROR, new PlayerListener(){ - @Override - public void playEnded() { - NarratorFactory.getInstance().speak(message); - } - }); - 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]); + public void insert(){ + DiagramPanel dPanel = getActiveTab(); + final DiagramTree tree = dPanel.getTree(); + DiagramTreeNode treeNode = (DiagramTreeNode)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); - 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; + /* 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,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.INSERT_EDGE,edge.getId(),edge.getName())); + } + }else{ // adding a Node + iLog("insert node ",DiagramElement.toLogString(diagramElement)); + modelUpdater.insertInTree(diagramElement); + } + } catch (ConnectNodesException cne) { + final String message = cne.getLocalizedMessage(); + SoundFactory.getInstance().play(SoundEvent.ERROR, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(message); + } + }); + 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, new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.ADD_PROPERTY,n.getId(),n.getName()))){ + 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 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); - } - } + 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, DiagramEventSource.TREE); + }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,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.ADD_PROPERTY,n.getId(),n.getName())); + }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,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_MODIFIERS,n.getId(),n.getName()))){ + 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; + } - 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") - }; + 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,DiagramEventSource.TREE); + } + modelUpdater.yieldLock(n, Lock.PROPERTIES,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_MODIFIERS,n.getId(),n.getName())); + } + }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,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_ENDLABEL,e.getId(),e.getName()))){ + 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",""); - 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()]; + 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,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_ENDLABEL,e.getId(),e.getName())); + 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,DiagramEventSource.TREE); + SoundFactory.getInstance().play(SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }); + }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_ENDDESCRIPTION_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,DiagramEventSource.TREE); + }else{ + iLog("cancel edge arrow head dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + } + modelUpdater.yieldLock(e, Lock.EDGE_END,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_ENDLABEL,e.getId(),e.getName())); + } + } + + public void delete(){ + DiagramPanel dPanel = getActiveTab(); + final DiagramTree tree = dPanel.getTree(); + final DiagramTreeNode treeNode = (DiagramTreeNode)tree.getSelectionPath().getLastPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + if(treeNode instanceof DiagramElement){ //delete a diagram element + final DiagramElement element = (DiagramElement)treeNode; + boolean isNode = element instanceof Node; + if(!modelUpdater.getLock(element, + Lock.DELETE, + new DiagramEventActionSource(DiagramEventSource.TREE, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + element.getId(),element.getName()))){ + 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 "+ (isNode ? "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 " + (isNode ? "node" : "edge") +" dialog",""); + modelUpdater.yieldLock(element, + Lock.DELETE, + new DiagramEventActionSource(DiagramEventSource.TREE, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + element.getId(), + element.getName())); + return; + } + modelUpdater.takeOutFromTree(element); + /* don't need to unlock because the object doesn't exist any more, but * + * still need to make other users aware that the deletion process is finished */ + modelUpdater.sendAwarenessMessage( + AwarenessMessage.Name.STOP_A, + new DiagramEventActionSource(DiagramEventSource.TREE, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + element.getId(),element.getName()) + ); + }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, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.REMOVE_PROPERTY,n.getId(),n.getName()))){ + 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, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.REMOVE_PROPERTY,n.getId(),n.getName())); + return; + } + modelUpdater.removeProperty(n, typeNode.getType(), typeNode.getIndex(treeNode),DiagramEventSource.TREE); + }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(); + DiagramTreeNode treeNode = (DiagramTreeNode)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()}; + boolean isNode = element instanceof Node; + if(!modelUpdater.getLock(element, + Lock.NAME, + new DiagramEventActionSource(DiagramEventSource.TREE, + isNode ? Command.Name.SET_NODE_NAME : Command.Name.SET_EDGE_NAME,element.getId(),element.getName()))){ + 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 "+(isNode ? "node" : "edge")+" dialog",DiagramElement.toLogString(element)); + String name = SpeechOptionPane.showInputDialog(EditorFrame.this, + formatter.format(arg), + element.getName()); + if(name != null){ + modelUpdater.setName(element,name,DiagramEventSource.TREE); + }else{ + iLog("cancel rename "+(isNode ? "node" : "edge")+" dialog",DiagramElement.toLogString(element)); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + modelUpdater.yieldLock(element, + Lock.NAME, + new DiagramEventActionSource( + DiagramEventSource.TREE, + isNode ? Command.Name.SET_NODE_NAME : Command.Name.SET_EDGE_NAME, + element.getId(), + element.getName())); + }else if(treeNode instanceof PropertyMutableTreeNode){ + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)treeNode.getParent(); + Node n = (Node)typeNode.getNode(); + if(!modelUpdater.getLock(n, + Lock.PROPERTIES, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_PROPERTY,n.getId(),n.getName()))){ + 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,DiagramEventSource.TREE); + modelUpdater.yieldLock(n, + Lock.PROPERTIES, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_PROPERTY,n.getId(),n.getName())); + }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(); + DiagramTreeNode treeNode = (DiagramTreeNode)tree.getLastSelectedPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + + if(!modelUpdater.getLock(treeNode, + Lock.BOOKMARK, + DiagramEventActionSource.NULL)){ + 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, DiagramEventActionSource.NULL); + 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);// without listeners in order not to overwrite the speechdialog popping up again + NarratorFactory.getInstance().speakWholeText(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); + NarratorFactory.getInstance().speakWholeText(resources.getString("dialog.input.bookmark.text.already_existing")); + }else{ + tree.getModel().putBookmark(bookmark, treeNode,DiagramEventSource.TREE); + uniqueBookmarkChosen = true; + } + }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] - ); - + 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())); - } - }); + tree.getModel().removeBookmark(bookmark,DiagramEventSource.TREE); }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){ + } + modelUpdater.yieldLock(treeNode, Lock.BOOKMARK, DiagramEventActionSource.NULL); + } + + public void editNotes(){ + DiagramPanel dPanel = getActiveTab(); + DiagramTreeNode treeNode = (DiagramTreeNode)dPanel.getTree().getLastSelectedPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + if(!modelUpdater.getLock(treeNode, Lock.NOTES, DiagramEventActionSource.NULL)){ + 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,DiagramEventSource.TREE); + }else{ + iLog("cancel edit note dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + modelUpdater.yieldLock(treeNode, Lock.NOTES,DiagramEventActionSource.NULL); + } + + public void startServer(){ + iLog("server started",""); + /* If the awareness filter has not been created yet (by opening the awareness filter dialog) then create it */ + BroadcastFilter filter = BroadcastFilter.getInstance(); + if(filter == null) + try{ + filter = BroadcastFilter.createInstance(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(this, ioe.getLocalizedMessage()); + } + /* create the server */ + server = Server.createServer(); + try{ + server.init(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + ioe.getLocalizedMessage()); + iLog("error: starting server",ioe.getLocalizedMessage()); + return; + } + server.start(); + startServerMenuItem.setEnabled(false); + stopServerMenuItem.setEnabled(true); + if(getActiveTab() != null && (!(getActiveTab().getDiagram() instanceof NetDiagram))) + 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.setAwarenessPanelEnabled(false); + 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; + } + startServerMenuItem.setEnabled(true); + stopServerMenuItem.setEnabled(false); + shareDiagramMenuItem.setEnabled(false); + if(getActiveTab() != null) + fileCloseItem.setEnabled(true); + 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.local_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(),DiagramEventSource.NONE)); + dPanel.setDiagram(NetDiagram.wrapLocalHost(diagram,localSocket,server.getLocalhostQueue(diagram.getName()),netLocalDiagramExceptionHandler)); + shareDiagramMenuItem.setEnabled(false); + fileCloseItem.setEnabled(false); + dPanel.setAwarenessPanelEnabled(true); + } 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 unshareDiagram(){ + DiagramPanel dPanel = getActiveTab(); + if(dPanel.getDiagram() instanceof NetDiagram){ + if(clientConnectionManager != null && clientConnectionManager.isAlive()) + clientConnectionManager.addRequest(new RmDiagramRequest(dPanel.getDiagram().getName())); + } + } + + 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, + resources.getString("dialog.share_diagram.enter_address"), + PreferencesService.getInstance().get("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("dialog.share_diagram.wrong_ip")); + return; + }else{ + PreferencesService.getInstance().put("server.address", addr); + /* 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, resources.getString("dialog.error.no_connection_to_server")); + return; + } + /* download the diagram list */ + DiagramDownloader downloader = new DiagramDownloader( + channel, + addr, + DiagramDownloader.CONNECT_AND_DOWNLOAD_LIST_TASK + ); + iLog("open download diagram list dialog",""); + int option = SpeechOptionPane.showProgressDialog(EditorFrame.this, resources.getString("dialog.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.DOWNLOAD_DIAGRAM_TASK); + 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.isAlive()){ 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(); - } - } - } - } - - /** + 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,DiagramEventSource.TREE); + Diagram newDiagram = NetDiagram.wrapRemoteHost(diagram,clientConnectionManager,channel); + DiagramPanel dPanel = addTab(null,newDiagram); + /* enable awareness on the new diagram */ + dPanel.setAwarenessPanelEnabled(true); + /* make the network thread aware of the new shared diagram, from here on the messages received from the server will take effect */ + clientConnectionManager.addRequest(new ClientConnectionManager.AddDiagramRequest(channel, diagram)); + clientConnectionManager.addRequest(new SendAwarenessRequest(channel, new AwarenessMessage( + AwarenessMessage.Name.USERNAME_A, + newDiagram.getName(), + AwarenessMessage.getDefaultUserName() + ))); + } + }catch(RuntimeException rte){ + throw new RuntimeException(rte); + }catch(ExecutionException ee){ + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}; + /* 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(),""); + }catch(Exception exception){ + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}; + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + exception.getLocalizedMessage()); + iLog("error: "+exception.getMessage(),""); + } + } + } + } + + public void showAwarenessPanel(){ + DiagramPanel dPanel = getActiveTab(); + if(dPanel != null){ + dPanel.setAwarenessPanelVisible(true); + NarratorFactory.getInstance().speak(resources.getString("speech.awareness_panel.open")); + } + } + + public void hideAwarenessPanel(){ + DiagramPanel dPanel = getActiveTab(); + if(dPanel != null){ + dPanel.setAwarenessPanelVisible(false); + NarratorFactory.getInstance().speak(resources.getString("speech.awareness_panel.close")); + dPanel.getTree().requestFocus(); + } + } + + 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();} - } - } + */ + 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,null); + 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); - } - - /** + */ + 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); + } + + public void showAwarenessBroadcastDialog(){ + BroadcastFilter filter = BroadcastFilter.getInstance(); + if(filter == null) + try{ + filter = BroadcastFilter.createInstance(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(this, ioe.getLocalizedMessage()); + } + filter.showDialog(this); + } + + public void showAwarenessDisplayDialog(){ + DisplayFilter filter = DisplayFilter.getInstance(); + if(filter == null) + try{ + filter = DisplayFilter.createInstance(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(this, ioe.getLocalizedMessage()); + } + filter.showDialog(this); + } + + public void showAwarnessUsernameDialog(){ + String oldName = AwarenessMessage.getDefaultUserName(); + String newName = SpeechOptionPane.showInputDialog( + this, + resources.getString("dialog.input.awerness_username"), + oldName + ); + if(newName == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return; + } + + if(newName.trim().isEmpty()){ + SoundFactory.getInstance().play(SoundEvent.ERROR, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(resources.getString("speech.empty_userame")); + } + }); + return; + } + + if(!newName.equals(oldName)){//if the name hasn't changed don't issue any message + PreferencesService.getInstance().put("user.name", newName); + AwarenessMessage.setDefaultUserName(newName); + NarratorFactory.getInstance().speak(MessageFormat.format( + resources.getString("dialog.feedback.awareness_username"), + newName + )); + for(int i=0; i<editorTabbedPane.getTabCount(); i++){ + Diagram diagram = editorTabbedPane.getComponentAt(i).getDiagram(); + diagram.getModelUpdater().sendAwarenessMessage( + AwarenessMessage.Name.USERNAME_A, + newName);// send the new name only, the old name will be added by the server + } + } + } + + public void showLocalServerPortDialog(){ + showServerPortDialog("server.local_port","dialog.input.local_server_port","dialog.feedback.local_server_port"); + } + + public void showRemoteServerPortDialog(){ + showServerPortDialog("server.remote_port","dialog.input.remote_server_port","dialog.feedback.remote_server_port"); + } + + private void showServerPortDialog(String preferenceKey,String dialogMessage,String feedbackMessage){ + String oldPort = PreferencesService.getInstance().get(preferenceKey,Server.DEFAULT_LOCAL_PORT); + String newPort = SpeechOptionPane.showInputDialog(this, resources.getString(dialogMessage), oldPort); + if(newPort == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return; + } + + boolean badFormat = false; + try { + int port = Integer.parseInt(newPort); + if(port <= 0 || port > 65535) + badFormat = true; + }catch(NumberFormatException nfe){ + badFormat = true; + } + + if(badFormat){ + SoundFactory.getInstance().play(SoundEvent.ERROR, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(resources.getString("speech.bad_format_port")); + } + }); + return; + } + PreferencesService.getInstance().put(preferenceKey,newPort); + NarratorFactory.getInstance().speak(MessageFormat.format( + resources.getString(feedbackMessage),newPort)); + } + + /** 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); - - } - - /** + */ + 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()); - } - })); + 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 + /** + * 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 DiagramPanel addTab(String path, Diagram diagram){ + DiagramPanel diagramPanel = new DiagramPanel(diagram,editorTabbedPane); + diagramPanel.setFilePath(path); + diagramPanel.getTree().addTreeSelectionListener(treeSelectionListener); + diagramPanel.setAwarenessPanelListener(awarenessPanelListener); + /* update the haptics */ + haptics.addNewDiagram(diagramPanel.getDiagram().getName(), true); + for(Node n : diagram.getCollectionModel().getNodes()) + haptics.addNode(n.getBounds().getCenterX(), n.getBounds().getCenterY(), System.identityHashCode(n),null); + 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(),null); + } + /* install the listener that handling the haptics device and the one handling the audio feedback */ + diagram.getCollectionModel().addCollectionListener(hapticTrigger); + AudioFeedback audioFeedback = new AudioFeedback(diagramPanel.getTree()); + diagram.getCollectionModel().addCollectionListener(audioFeedback); + diagram.getTreeModel().addDiagramTreeNodeListener(audioFeedback); + + editorTabbedPane.add(diagramPanel); + editorTabbedPane.setToolTipTextAt(editorTabbedPane.getTabCount()-1,path);//the new panel is at tabCount -1 + editorTabbedPane.setSelectedIndex(editorTabbedPane.getTabCount()-1); + /* 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(); + getContentPane().requestFocusInWindow(); + return diagramPanel; + } - /* 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){ + private void diagramPanelEnabledMenuUpdate(DiagramPanel dPanel){ + fileSaveItem.setEnabled(false); + fileSaveAsItem.setEnabled(false); + fileCloseItem.setEnabled(false); + shareDiagramMenuItem.setEnabled(false); + graphExportItem.setEnabled(false); + showAwarenessPanelMenuItem.setEnabled(false); + hideAwarenessPanelMenuItem.setEnabled(false); + if(dPanel == null) + return; + + fileSaveItem.setEnabled(true); + fileSaveAsItem.setEnabled(true); + graphExportItem.setEnabled(true); + if(dPanel.getDiagram() instanceof NetDiagram){ + if(dPanel.isAwarenessPanelVisible()) + hideAwarenessPanelMenuItem.setEnabled(true); + else + showAwarenessPanelMenuItem.setEnabled(true); + } + + boolean isSharedDiagram = dPanel.getDiagram() instanceof NetDiagram; + if(server != null && !isSharedDiagram){ + shareDiagramMenuItem.setEnabled(true); + } + + if(!(isSharedDiagram && dPanel.getDiagram().getLabel().endsWith(NetDiagram.LOCALHOST_STRING))) + 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); + selectMenuItem.setEnabled(false); + if(path == null) + return; + + jumpMenuItem.setEnabled(true); + editNotesMenuItem.setEnabled(true); + bookmarkMenuItem.setEnabled(true); + + /* jump to reference : a reference node must be selected */ + DiagramTreeNode treeNode = (DiagramTreeNode)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); + if(treeNode instanceof Node) + selectMenuItem.setEnabled(true); + } + } + + private boolean readTemplateFiles(File[] files){ + boolean someFilesNotRead = false; + for(File file : files){ try { Diagram d = PersistenceManager.decodeDiagramTemplate(file); addDiagramType(d); @@ -1893,61 +2116,65 @@ e.printStackTrace(); } } - return someFilesNotRead; - } - - private void iLog(String action,String args){ - InteractionLog.log("TREE",action,args); - } - - private void iLog(String message){ - InteractionLog.log(message); - } + return someFilesNotRead; + } - 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 void iLog(String action,String args){ + InteractionLog.log("TREE",action,args); + } - private ExtensionFilter extensionFilter; - private ExtensionFilter exportFilter; + private void iLog(String message){ + InteractionLog.log(message); + } - private static final int DEFAULT_MAX_RECENT_FILES = 5; - private static final double GROW_SCALE_FACTOR = Math.sqrt(2); - + 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 AwarenessPanelEnablingListener awarenessPanelListener; + + 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 selectMenuItem; + private JMenuItem bookmarkMenuItem; + private JMenuItem editNotesMenuItem; + private JMenuItem locateMenuItem; + private JMenuItem highlightMenuItem; + private JMenuItem shareDiagramMenuItem; + private JMenuItem startServerMenuItem; + private JMenuItem stopServerMenuItem; + private JMenuItem showAwarenessPanelMenuItem; + private JMenuItem hideAwarenessPanelMenuItem; + 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); + }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.properties Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.properties Wed Apr 25 17:09:09 2012 +0100 @@ -1,7 +1,6 @@ ### APPLICATION ### app.name=CCmI Editor -app.copyright=Copyright (C) 2002...2005 Cay S. Horstmann (http://horstmann.com) -app.version=0.1a +app.version=0.1.4 files.name=CCmI Diagram files files.extension=.ccmi template.extension=.xml @@ -10,7 +9,6 @@ ### EDITOR ### -error.version=You need Java version {0} files.image.name=Image Files files.image.extension=.jpg grabber.text=Select @@ -18,6 +16,8 @@ window.focus=Editor window focused window.unfocus=Editor window unfocused window.tab=Tab, {0} +window.tree=Tree +window.no_tabs=No diagrams open #### DIALOGS ### dialog.about={0} version {1}\u000A\u000A{2}\u000A\u000A{3} @@ -43,16 +43,18 @@ 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.same_file_name=Cannot have two diagrams with the same name open at the same time 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.error.no_connection_to_server=Could not connect to the server dialog.template_created=Diagram {0} created dialog.file_saved=File Saved dialog.downloading_diagram=Downloading diagram: {0} +dialog.downloading_diagram_list=Downloading diagram list dialog.warning.title=Warning dialog.warning.null_modifiers=Nothing to edit for {0} @@ -72,8 +74,8 @@ 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.label=Set label +dialog.input.edge_operation.arrow_head=Set 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 @@ -82,6 +84,9 @@ 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.input.awerness_username=Enter Awareness User Name +dialog.input.local_server_port=Enter local server port number +dialog.input.remote_server_port=Enter remote server port number 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 ? @@ -115,10 +120,16 @@ dialog.speech_option_pane.message= {0}. Press OK to confirm dialog.speech_option_pane.cancel=Cancel +dialog.share_diagram.wrong_ip=invalid IP address +dialog.share_diagram.enter_address="Enter server address" + dialog.file_chooser.file_type=File Type: dialog.file_chooser.file_name=file Name: -dialog.speech_rate.message=Speech rate set to {0} +dialog.feedback.speech_rate=Speech rate set to {0} +dialog.feedback.awareness_username=User Name set to {0} +dialog.feedback.local_server_port=Local server port number set to {0} +dialog.feedback.remote_server_port=Remote server port number set to {0} server.shutdown_msg=request by user server.not_running_exc=Server not running @@ -136,6 +147,7 @@ speech.note.updated=Note updated speech.empty_property=Empty string, nothing created. {0} speech.empty_label=Empty label +speech.empty_userame=User Name cannot be empty speech.selected=selected speech.unselected=unselected speech.input.property.ack={0} created, @@ -143,17 +155,33 @@ speech.input.edge.ack=, connected, speech.input.edge.ack2= and speech.invalid_ip=invalid IP address +speech.haptic_device_crashed=Haptic device crashed. Unsaved diagrams saved in backup directory +speech.bad_format_port=Bad port number format speech.diagram_closed= {0} closed. speech.node_selected={0} selected speech.node_unselected={0} unselected speech.jump=Jump to {0} +speech.awareness_panel.open=Awareness panel open +speech.awareness_panel.close=Awareness panel close + ### TABBED PANE ### tab.new_tab=new {0} tab.new_tab_id=new {0} ({1}) ### MENU ### + +menufactory.selected={0} selected +menufactory.unselected={0} unselected +menufactory.3dot= dot dot dot +menufactory.disabled=, disabled +menufactory.ctrl=, control +menufactory.menu=menu +menufactory.submenu=sub menu +menufactory.leaving=Leaving Menu + + file.text=File file.mnemonic=F file.new.text=New Diagram @@ -192,6 +220,7 @@ edit.rename.text=Rename edit.rename.mnemonic=R edit.rename.accelerator=ctrl R +edit.select.text=Select edit.bookmark.text=Add/Remove Bookmark edit.bookmark.mnemonic=B edit.bookmark.accelerator=ctrl B @@ -213,13 +242,6 @@ 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 @@ -248,6 +270,30 @@ collab.stop_server.text=Stop Server collab.share_diagram.text=Share Diagram collab.open_shared_diagram.text= Open Shared Diagram +collab.unshare_diagram.text=Stop Diagram Sharing +collab.show_awareness_panel.text=Show Awareness Panel +collab.hide_awareness_panel.text=Hide Awareness Panel +preferences.text=Preferences +preferences.mnemonic=P +preferences.awareness.text=Awareness +preferences.awareness.mnemonic=A +preferences.awareness.username.text=User Name +preferences.awareness.broadcast.text=Broadcast Filter +preferences.awareness.display.text=Display Filter +preferences.awareness.enable_voice.text=Enable Awareness Voice +preferences.file_chooser.text=Enable accessible file chooser +preferences.sound.text=Sound +preferences.sound.mnemonic=S +preferences.sound.mute.text=Mute +preferences.sound.mute.mnemonic=M +preferences.sound.mute.accelerator=ctrl M +preferences.sound.rate.text=Set Speech Rate +preferences.sound.rate.mnemonic=R +preferences.server.text=Networking +preferences.local_server.port.text=Local Server Port +preferences.remote_server.port.text=Remote Server Port + + help.text=Help help.mnemonic=H help.about.text=About
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorTabbedPane.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorTabbedPane.java Wed Apr 25 17:09:09 2012 +0100 @@ -18,67 +18,36 @@ */ 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; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; /** * - * The tabbed pane of the editor. On each tab a {@link DiagramPanel} is displayed. + * The tabbed pane of the editor. On each tab a {@code DiagramPanel} is displayed. * */ @SuppressWarnings("serial") public class EditorTabbedPane extends JTabbedPane { - public EditorTabbedPane(JFrame frame){ - this.frame = frame; + /** + * Creates a new {@code EditorTabbedPane} + * + * @param frame the frame when this tabbed pane will be placed + */ + public EditorTabbedPane(EditorFrame 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()); - } - }); + SpeechUtilities.changeTabListener(this,frame); + getAccessibleContext().setAccessibleName("tab "); + /* shut up the narrator upon pressing ctrl */ getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"ctrldown"); getActionMap().put("ctrldown",new AbstractAction(){ @@ -88,6 +57,12 @@ }); } + /** + * Sets the title of the tab containing a component. + * + * @param component the component in the tab whose title has to be set + * @param title the new title + */ public void setComponentTabTitle(Component component, String title){ int index = indexOfComponent(component); if(index == -1) @@ -95,6 +70,12 @@ setTitleAt(index,title); } + /** + * Returns the title of the tab containing a component. + * + * @param component the component contained by the tab + * @return the title of the tab containing {@code component} + */ public String getComponentTabTitle(Component component){ int index = indexOfComponent(component); if(index == -1) @@ -102,6 +83,11 @@ return getTitleAt(index); } + /** + * Repaints the title on a tab containing a component. + * + * @param component the component contained by the tab whose title will be repainted + */ public void refreshComponentTabTitle(Component component){ setComponentTabTitle(component,component.getName()); } @@ -110,7 +96,16 @@ public DiagramPanel getComponentAt(int n){ return (DiagramPanel)super.getComponent(n); } - + + /** + * The components in an {@code EditorTabbedPane} are all instances of {@code DiagramPanel}, which in turns + * holds an instance of {@code Diagram}. This utility methods retrieves the index of + * the {@code DiagramPanel} whose diagram has the same name than the {@code String} passed as argument. + * + * @param diagramName the name of the diagram to look for + * @return the index of the diagram named as {@code diagramName} or {@code -1} if + * such diagram doesn't exist + */ public int getDiagramNameTabIndex(String diagramName){ for(int i=0; i<getTabCount();i++){ DiagramPanel dPanel = getComponentAt(i); @@ -121,6 +116,15 @@ return -1; } + /** + * The components in an {@code EditorTabbedPane} are all instances of {@code DiagramPanel}, which in turns + * holds an instance of {@code Diagram}. This method returns the index of the {@code DiagramPanel} + * whose diagram has been saved on the file system at the path specified as argument. + * + * @param path + * @return the index of the {@code DiagramPanel} whose diagram has been saved on + * the file system in {@code path}, or {@code -1} if such diagram doesn't exist + */ public int getPathTabIndex(String path){ for(int i=0; i<getTabCount();i++){ DiagramPanel dPanel = getComponentAt(i); @@ -130,7 +134,4 @@ } return -1; } - - private JFrame frame; - }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/FileService.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/FileService.java Wed Apr 25 17:09:09 2012 +0100 @@ -1,3 +1,21 @@ +/* + 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; @@ -24,193 +42,215 @@ 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; - - } + /** + * 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 ; - /** - * 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(); - } + /** + * 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; - /** - * 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); - } - } - + /** + * 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(); + } - /* 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); - } - } + /** + * 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); + } - 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; - } - - /** + public FileService.Open open(String defaultDirectory, String defaultFile, + ExtensionFilter filter, Frame frame) throws FileNotFoundException { + checkChangedOption(); + 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 the user cancels the task (presses cancel button or the X at the top left corner) * + * the CANCEl sound is played (together with the registered playerListeenr if any) */ + 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, String[] currentTabs) throws IOException { + checkChangedOption(); + 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){ + ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + File f = fileChooser.getSelectedFile(); + if (addExtension != null && f.getName().indexOf(".") < 0) // no extension supplied + f = new File(f.getPath() + addExtension); + + String fileName = getFileNameFromPath(f.getAbsolutePath(),false); + for(String tab : currentTabs){ + if(fileName.equals(tab)) + throw new IOException(resources.getString("dialog.error.same_file_name")); + } + + if (!f.exists()) // file doesn't exits return the new SaveImpl with no problems + return new SaveImpl(f); + + /* file with this name already exists, we must ask the user to confirm */ + 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); + /* returned if the user doesn't want to overwrite the file */ + return new SaveImpl(null); + } + + /* check if the user has changed the configuration since the last time a the fileChooser was shown */ + private void checkChangedOption(){ + boolean useAccessible = Boolean.parseBoolean(PreferencesService.getInstance().get("use_accessible_filechooser", "true")); + if(this.useAccessible != useAccessible){ + this.useAccessible = useAccessible; + File currentDir = fileChooser.getCurrentDirectory(); + fileChooser = FileChooserFactory.getFileChooser(useAccessible); + fileChooser.setCurrentDirectory(currentDir); + } + } + + 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 @@ -221,34 +261,34 @@ 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; - } + */ + 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; + } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/Finder.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Finder.java Wed Apr 25 17:09:09 2012 +0100 @@ -22,7 +22,7 @@ import java.util.Collection; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; -import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModelTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNode; /** * @@ -101,8 +101,8 @@ * @param root * @return */ - public static DiagramModelTreeNode findTreeNode(int[] path, DiagramModelTreeNode root){ - DiagramModelTreeNode retVal = root; + public static DiagramTreeNode findTreeNode(int[] path, DiagramTreeNode root){ + DiagramTreeNode retVal = root; for(int i=0;i<path.length;i++){ if(retVal.getChildCount() <= path[i]) return null;
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphElement.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphElement.java Wed Apr 25 17:09:09 2012 +0100 @@ -24,24 +24,69 @@ /** - * 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. + * An interface implemented by {@code Node}, {@code Edge} and {@code Edge.InnerPoint}. It defines methods that + * all objects painted on a graph share. The interface is used mainly + * for convenience in treating the different types of object in a uniformly. * */ public interface GraphElement { + /** + * Draw the graphic element on a canvas + * + * @param g2 the graphics object. Use {@code g2.draw()} to get things painted on the graph. + */ public void draw(Graphics2D g2); - public void translate(Point2D p, double dx, double dy); + /** + * Translates this graphic element on the graph + * + * @param p the starting point where the translation starts (normally where the user clicks + * with their mouse) + * @param dx the distance to translate along the x-axis + * @param dy the distance to translate along the y-axis + * @param source the source of the translate action + */ + public void translate(Point2D p, double dx, double dy, Object source); - public void stopMove(); + /** + * This method is to be called before translation or any other operation that changes the + * position of the graph element or any of its parts. + * + * @param p the starting point of the motion + * @param source the source of the motion action + */ + public void startMove(Point2D p, Object source); + + /** + * This method is to be called when the motion (e.g. a translation) is over. + * Note that for instance a translation might be composed on several calls to {@code translate} + * + * @param source + */ + public void stopMove(Object source); - public void startMove(Point2D p); - + /** + * Gets the bounding {@code Rectangle} of this graph element. + * + * @return a new {@code Rectangle} equals to the bounding {@code Rectangle} of this graph element + */ public Rectangle2D getBounds(); + /** + * Returns the point where an line, with a specified direction, would come in contact with the outline of this + * graph element. + * + * @param d the direction of the line + * @return a new point on the outline if this graph element where the line comes in contact with it + */ public Point2D getConnectionPoint(Direction d); + /** + * Tests if a specified {@code Point2D} is inside the boundary of this graph element. + * + * @param p the point to be tested + * @return {@code true} if the point is inside the boundary, {@code false} otherwise + */ public boolean contains(Point2D p); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphPanel.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphPanel.java Wed Apr 25 17:09:09 2012 +0100 @@ -24,9 +24,7 @@ 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; @@ -40,19 +38,19 @@ 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.DiagramTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.network.Command; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; /** @@ -89,73 +87,6 @@ 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 @@ -170,7 +101,7 @@ 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 + if(e.getDiagramElement() instanceof Node && e.getSource().equals(DiagramEventSource.GRPH)){ setElementSelected(e.getDiagramElement()); dragMode = DRAG_NODE; } @@ -181,12 +112,12 @@ public void elementTakenOut(final CollectionEvent e) { DiagramElement element = e.getDiagramElement(); if(element instanceof Node){ - if(nodePopup != null && nodePopup.nodeRef.equals(element)) + if(nodePopup != null && nodePopup.getElement().equals(element)) nodePopup.setVisible(false); nodes.remove(element); } else{ - if(edgePopup != null && edgePopup.edgeRef.equals(element)) + if(edgePopup != null && edgePopup.getElement().equals(element)) edgePopup.setVisible(false); edges.remove(element); } @@ -229,17 +160,17 @@ 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); + CCmIPopupMenu.EdgePopupMenu pop = new CCmIPopupMenu.EdgePopupMenu(e,GraphPanel.this,modelUpdater,selectedElements); 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); + CCmIPopupMenu.EdgePopupMenu pop = new CCmIPopupMenu.EdgePopupMenu(e,extremityNode,GraphPanel.this,modelUpdater,selectedElements); edgePopup = pop; pop.show(GraphPanel.this, event.getX(), event.getY()); } } }else if(n != null){ - NodePopupMenu pop = new NodePopupMenu(n,GraphPanel.this,modelUpdater); + CCmIPopupMenu.NodePopupMenu pop = new CCmIPopupMenu.NodePopupMenu(n,GraphPanel.this,modelUpdater,selectedElements); nodePopup = pop; pop.show(GraphPanel.this, event.getX(), event.getY()); }else @@ -249,10 +180,11 @@ /* - one click && palette == select - */ else if (tool == null){ if(n != null){ // node selected - if (isCtrl) + if (isCtrl){ addElementToSelection(n,false); - else + }else{ setElementSelected(n); + } dragMode = DRAG_NODE; }else if (e != null){ // edge selected if (isCtrl){ @@ -260,7 +192,7 @@ dragMode = DRAG_NODE; }else{ setElementSelected(e); - modelUpdater.startMove(e, mousePoint); + modelUpdater.startMove(e, mousePoint,DiagramEventSource.GRPH); dragMode = DRAG_EDGE; } }else{ // nothing selected : make selection lasso @@ -284,11 +216,11 @@ Rectangle2D bounds = newNode.getBounds(); /* perform the translation from the origin */ newNode.translate(new Point2D.Double(), mousePoint.getX() - bounds.getX(), - mousePoint.getY() - bounds.getY()); + mousePoint.getY() - bounds.getY(),DiagramEventSource.NONE); /* 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); + modelUpdater.insertInCollection(newNode,DiagramEventSource.GRPH); } } @@ -304,21 +236,31 @@ (event.getY()+minY)/zoom ); if(lastSelected != null){ - if(lastSelected instanceof Node){ + if(lastSelected instanceof Node || selectedElements.size() > 1){ // differentiate between translate and edge bending if(wasMoving){ iLog("move selected stop",mousePoint.getX()+" "+ mousePoint.getY()); for(Object element : moveLockedElements){ - modelUpdater.stopMove((GraphElement)element); - modelUpdater.yieldLock((DiagramModelTreeNode)element, Lock.MOVE); + modelUpdater.stopMove((GraphElement)element,DiagramEventSource.GRPH); + boolean isNode = element instanceof Node; + modelUpdater.yieldLock((DiagramTreeNode)element, + Lock.MOVE, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + isNode ? Command.Name.STOP_NODE_MOVE : Command.Name.STOP_EDGE_MOVE, + ((DiagramElement)element).getId(),((DiagramElement)element).getName())); } moveLockedElements.clear(); } - }else{ // instanceof Edge + }else{ // instanceof Edge && selectedelements.size() = 1. Bending if(wasMoving){ iLog("bend edge stop",mousePoint.getX()+" "+ mousePoint.getY()); if(moveLockedEdge != null){ - modelUpdater.stopMove(moveLockedEdge); - modelUpdater.yieldLock(moveLockedEdge, Lock.MOVE); + modelUpdater.stopMove(moveLockedEdge,DiagramEventSource.GRPH); + modelUpdater.yieldLock(moveLockedEdge, Lock.MOVE, new DiagramEventActionSource( + DiagramEventSource.GRPH, + Command.Name.BEND, + moveLockedEdge.getId(), + moveLockedEdge.getName())); moveLockedEdge = null; } } @@ -348,7 +290,14 @@ Iterator<DiagramElement> iterator = selectedElements.iterator(); while(iterator.hasNext()){ DiagramElement element = iterator.next(); - if(modelUpdater.getLock(element, Lock.MOVE)){ + boolean isNode = element instanceof Node; + if(modelUpdater.getLock(element, + Lock.MOVE, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + isNode ? Command.Name.TRANSLATE_NODE : Command.Name.TRANSLATE_EDGE, + element.getId(), + element.getName()))){ moveLockedElements.add(element); }else{ iLog("Could not get move lock for element",DiagramElement.toLogString(element)); @@ -360,21 +309,29 @@ for (DiagramElement selected : selectedElements){ if(selected instanceof Node) - modelUpdater.translate((Node)selected, lastMousePoint, dx, dy); + modelUpdater.translate((Node)selected, lastMousePoint, dx, dy,DiagramEventSource.GRPH); else - modelUpdater.translate((Edge)selected, lastMousePoint, dx, dy); + modelUpdater.translate((Edge)selected, lastMousePoint, dx, dy,DiagramEventSource.GRPH); } } else if(dragMode == DRAG_EDGE){ if(!wasMoving){ wasMoving = true; - if(modelUpdater.getLock(lastSelected, Lock.MOVE)) + if(modelUpdater.getLock(lastSelected, + Lock.MOVE, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + Command.Name.BEND, + lastSelected.getId(), + lastSelected.getName())) + ){ moveLockedEdge = (Edge)lastSelected; - else + }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())); + modelUpdater.bend(moveLockedEdge, new Point2D.Double(mousePoint.getX(), mousePoint.getY()),DiagramEventSource.GRPH); } else if (dragMode == DRAG_LASSO){ double x1 = mouseDownPoint.getX(); double y1 = mouseDownPoint.getY(); @@ -698,7 +655,8 @@ } try { e.connect(nodesToConnect); - modelUpdater.insertInCollection(e); + /* perform the command, no lock is needed for inserting */ + modelUpdater.insertInCollection(e,DiagramEventSource.GRPH); } catch (ConnectNodesException cnEx) { JOptionPane.showMessageDialog(GraphPanel.this, cnEx.getLocalizedMessage(), @@ -716,8 +674,8 @@ private Grid grid; private GraphToolbar toolbar; - private NodePopupMenu nodePopup; - private EdgePopupMenu edgePopup; + private CCmIPopupMenu.NodePopupMenu nodePopup; + private CCmIPopupMenu.EdgePopupMenu edgePopup; private double zoom; private double gridSize;
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/HapticKindle.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/HapticKindle.java Wed Apr 25 17:09:09 2012 +0100 @@ -20,6 +20,7 @@ import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import java.util.ResourceBundle; import javax.swing.SwingUtilities; @@ -27,8 +28,11 @@ 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.main.DiagramEditorApp; +import uk.ac.qmul.eecs.ccmi.network.Command; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; 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; @@ -45,94 +49,94 @@ unselectRunnable = new Runnable(){ @Override public void run(){ + EditorFrame frame = DiagramEditorApp.getFrame(); + if((frame == null)||(frame.getActiveTab() == null)) + return; frame.selectHapticHighligh(null); } }; - frameBackupRunnable = new Runnable(){ - @Override - public void run(){ - frame.backupOpenDiagrams(); - } - }; } - public void setEditorFrame(EditorFrame frame){ - this.frame = frame; - } - + /** + * Implementation of the {@code executeCommand} method. All the commands that involve the + * diagram model, are executed in the Event Dispatching Thread through {@code SwingUtilities} invoke + * methods. This prevents race conditions on the model and on diagram elements. + * + * @see HapticListener#executeCommand(HapticListenerCommand, int, double, double, double, double) + */ @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; + public void executeCommand(HapticListenerCommand cmd, final int ID, final double x, final double y, final double startX, final double startY) { + final EditorFrame frame = DiagramEditorApp.getFrame(); 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()); + case PLAY_ELEMENT_SOUND : + SwingUtilities.invokeLater(new Runnable(){ + public void run(){ + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement 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()); + SwingUtilities.invokeLater(new Runnable(){ + public void run(){ + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement 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); + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + if(de == null) + return; + frame.selectHapticHighligh(de); } }); 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; - } - + case MOVE : { + /* when this block is executed we already have the lock * + * on the element from the PICK_UP command execution */ try { SwingUtilities.invokeAndWait(new Runnable(){ @Override public void run(){ - if(moveSelectedElement instanceof Node){ - Node n = (Node)moveSelectedElement; + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + if(de == null) + return; + DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater(); + if(de instanceof Node){ + Node n = (Node)de; 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); + n.getMonitor().lock(); + modelUpdater.translate(n, p, dx, dy,DiagramEventSource.HAPT); + modelUpdater.stopMove(n,DiagramEventSource.HAPT); + n.getMonitor().unlock(); StringBuilder builder = new StringBuilder(); builder.append(DiagramElement.toLogString(n)).append(" ").append(p.getX()) @@ -141,16 +145,16 @@ builder = new StringBuilder(); builder.append(DiagramElement.toLogString(n)).append(' ') .append(x).append(' ').append(y); - iLog("move node end",builder.toString()); + }else{ + Edge e = (Edge)de; + modelUpdater.startMove(e, new Point2D.Double(startX,startY),DiagramEventSource.HAPT); + Point2D p = new Point2D.Double(x,y); + e.getMonitor().lock(); + modelUpdater.bend(e, p,DiagramEventSource.HAPT); + modelUpdater.stopMove(e,DiagramEventSource.HAPT); + e.getMonitor().unlock(); - }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); @@ -159,25 +163,37 @@ builder.append(DiagramElement.toLogString(e)).append(' ') .append(x).append(' ').append(y); iLog("bend edge end",builder.toString()); - } - } + modelUpdater.yieldLock(de, + Lock.MOVE, + new DiagramEventActionSource( + DiagramEventSource.HAPT, + de instanceof Node ? Command.Name.STOP_NODE_MOVE : Command.Name.STOP_EDGE_MOVE, + de.getId(), + de.getName() + )); + } // run() }); } 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()); + case INFO : + SwingUtilities.invokeLater(new Runnable(){ + public void run(){ + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement 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) ){ @@ -189,19 +205,50 @@ 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 PICK_UP : + try { + SwingUtilities.invokeAndWait(new Runnable (){ + @Override + public void run(){ + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + if(de == null) + return; + DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater(); + if(!modelUpdater.getLock(de, + Lock.MOVE, + new DiagramEventActionSource(DiagramEventSource.HAPT, de instanceof Edge ? Command.Name.TRANSLATE_EDGE : Command.Name.TRANSLATE_NODE ,de.getId(),de.getName()))){ + iLog("Could not get lock on element for motion", DiagramElement.toLogString(de)); + NarratorFactory.getInstance().speak("Object is being moved by another user"); + return; + } + frame.hPickUp(de); + SoundFactory.getInstance().play(SoundEvent.HOOK_ON); + iLog("hook on",""); + } + }); + }catch(Exception e){ + e.printStackTrace(); + throw new RuntimeException(); + } + 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"); + SwingUtilities.invokeLater(new Runnable(){ + @Override + public void run(){ + if((frame == null)||(frame.getActiveTab() == null)) + return; + frame.backupOpenDiagrams(); + } + }); + NarratorFactory.getInstance().speak(ResourceBundle.getBundle(EditorFrame.class.getName()).getString("speech.haptic_device_crashed")); break; } } @@ -211,7 +258,6 @@ } private Runnable unselectRunnable; - private Runnable frameBackupRunnable; private static String INTERACTION_LOG_SOURCE = "HAPTIC"; - private EditorFrame frame; + //private EditorFrame frame; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/HapticTrigger.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/HapticTrigger.java Wed Apr 25 17:09:09 2012 +0100 @@ -33,40 +33,51 @@ @Override public void elementInserted(CollectionEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); if(evt.getDiagramElement() instanceof Node){ Node n = (Node)evt.getDiagramElement(); - HapticsFactory.getInstance().addNode(n.getBounds().getCenterX(), n.getBounds().getCenterY(), System.identityHashCode(n)); + HapticsFactory.getInstance().addNode(n.getBounds().getCenterX(), n.getBounds().getCenterY(), System.identityHashCode(n),source.getDiagramName()); }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()); + HapticsFactory.getInstance().addEdge(System.identityHashCode(e),pr.xs,pr.ys,pr.adjMatrix,pr.nodeStart,e.getStipplePattern(),e.getNameLine(),source.getDiagramName()); } } @Override public void elementTakenOut(CollectionEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); if(evt.getDiagramElement() instanceof Node){ Node n = (Node)evt.getDiagramElement(); - HapticsFactory.getInstance().removeNode(System.identityHashCode(n)); + HapticsFactory.getInstance().removeNode(System.identityHashCode(n),source.getDiagramName()); }else{//edge Edge e = (Edge)evt.getDiagramElement(); - HapticsFactory.getInstance().removeEdge(System.identityHashCode(e)); + HapticsFactory.getInstance().removeEdge(System.identityHashCode(e),source.getDiagramName()); } } @Override public void elementChanged(ElementChangedEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); 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()); + HapticsFactory.getInstance().updateEdge( + System.identityHashCode(e), + pr.xs, + pr.ys, + pr.adjMatrix, + pr.nodeStart, + e.getNameLine(), + source.getDiagramName()); }else{ Node n = (Node)evt.getDiagramElement(); HapticsFactory.getInstance().moveNode( n.getBounds().getCenterX(), n.getBounds().getCenterY(), - System.identityHashCode(n) + System.identityHashCode(n), + source.getDiagramName() ); } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/LineStyle.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/LineStyle.java Wed Apr 25 17:09:09 2012 +0100 @@ -27,27 +27,48 @@ * */ public enum LineStyle { - Solid(new BasicStroke()), + Solid(new BasicStroke(),0xFFFF), Dotted(new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0.0f, new float[]{1.0f,3.0f}, - 0.0f)), + 0.0f),0xF0F0), Dashed(new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0.0f, new float[]{5.0f,5.0f}, - 0.0f)); + 0.0f),0xAAAA); - private LineStyle(BasicStroke stroke){ + private LineStyle(BasicStroke stroke, int stipplePattern){ this.stroke = stroke; + this.stipplePattern = stipplePattern; } + /** + * returns the stroke of this line style. The stroke is used to paint + * the edge that has this line style on a graphics. + * + * @return the stroke for this line style + */ public Stroke getStroke(){ return stroke; } + /** + * Returns an a bit representation of the stippling of this edge. + * This value can be used by openGL like libraries to draw the edge and it's used by + * the OmniHaptic device native code to paint the edge visually and haptically. + * See also {@link http://www.opengl.org/sdk/docs/man/xhtml/glLineStipple.xml} + * + * + * @return an int with the bit representation of the stipple pattern + */ + public int getStipplePattern(){ + return stipplePattern; + } + private Stroke stroke; + private int stipplePattern; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/Node.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Node.java Wed Apr 25 17:09:09 2012 +0100 @@ -35,6 +35,7 @@ import org.w3c.dom.NodeList; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramEdge; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; @@ -112,12 +113,12 @@ } @Override - public void stopMove(){ - notifyChange(new ElementChangedEvent(this,this,"stop_move")); + public void stopMove(Object source){ + notifyChange(new ElementChangedEvent(this,this,"stop_move",source)); /* 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(); + getEdgeAt(i).stopMove(source); } } @@ -127,17 +128,25 @@ * @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){ + public void translate( Point2D p , double dx, double dy, Object source){ translateImplementation( p, dx, dy); - notifyChange(new ElementChangedEvent(this, this, "translate")); + for(int i=0; i< getInternalNodesNum();i++){ + getInternalNodeAt(i).translate(p, dx, dy,source); + } + notifyChange(new ElementChangedEvent(this, this, "translate", source)); } - protected void translateImplementation(Point2D p , double dx, double dy){ - for(int i=0; i< getInternalNodesNum();i++){ - getInternalNodeAt(i).translate(p, dx, dy); - } + /** + * @see DiagramTreeNode#setNotes(String) + */ + @Override + protected void setNotes(String notes,Object source){ + this.notes = notes; + notifyChange(new ElementChangedEvent(this,this,"notes",source)); } - + + protected abstract void translateImplementation(Point2D p , double dx, double dy); + /** * Tests whether the node contains a point. * @param aPoint the point to test @@ -153,7 +162,7 @@ public abstract Rectangle2D getBounds(); @Override - public void startMove(Point2D p){ + public void startMove(Point2D p,Object source){ /* useless, here just to comply with the GraphElement interface */ } @@ -223,7 +232,7 @@ } public void decode(Document doc, Element nodeTag) throws IOException{ - setName(nodeTag.getAttribute(PersistenceManager.NAME)); + setName(nodeTag.getAttribute(PersistenceManager.NAME),DiagramEventSource.PERS); try{ setId(Integer.parseInt(nodeTag.getAttribute(PersistenceManager.ID))); }catch(NumberFormatException nfe){ @@ -241,7 +250,7 @@ throw new IOException(); } Rectangle2D bounds = getBounds(); - translate(new Point2D.Double(0,0), dx - bounds.getX(), dy - bounds.getY()); + translate(new Point2D.Double(0,0), dx - bounds.getX(), dy - bounds.getY(),DiagramEventSource.PERS); NodeList propList = nodeTag.getElementsByTagName(PersistenceManager.PROPERTY); NodeProperties properties = getProperties(); @@ -307,12 +316,12 @@ throw new IOException(nfe); } } - addProperty(propertyType, value);//whether propertyType actually exist in the prototypes has been already checked - setModifierIndexes(propertyType, h, indexesToAdd); + addProperty(propertyType, value,DiagramEventSource.PERS);//whether propertyType actually exist in the prototypes has been already checked + setModifierIndexes(propertyType, h, indexesToAdd,DiagramEventSource.PERS); }else - addProperty(propertyType, value); + addProperty(propertyType, value,DiagramEventSource.PERS); }else - addProperty(propertyType, value); + addProperty(propertyType, value,DiagramEventSource.PERS); } } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/NodePopupMenu.java Mon Feb 06 12:54:06 2012 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,112 +0,0 @@ -/* - 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.trim()); - 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()); -}
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/NodePopupMenu.properties Mon Feb 06 12:54:06 2012 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ - - -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
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/PropertyEditorDialog.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/PropertyEditorDialog.java Wed Apr 25 17:09:09 2012 +0100 @@ -177,9 +177,7 @@ 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 != null) - value = value.trim(); + String value = model.getValueAt(j, 0).toString().trim(); if(!value.equals("")){ properties.addValue(model.getColumnName(0),value , model.getIndexesAt(j)); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechLogDialog.java Mon Feb 06 12:54:06 2012 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/* - 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; -}
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechMenuFactory.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechMenuFactory.java Wed Apr 25 17:09:09 2012 +0100 @@ -20,8 +20,12 @@ package uk.ac.qmul.eecs.ccmi.gui; import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; +import java.text.MessageFormat; +import java.util.ResourceBundle; import javax.swing.AbstractAction; import javax.swing.Action; @@ -33,8 +37,8 @@ import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; -import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; /** @@ -53,7 +57,7 @@ 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"); + NarratorFactory.getInstance().speak(resources.getString("menufactory.leaving")); } } }; @@ -71,11 +75,11 @@ public void menuSelectionChanged(boolean isIncluded){ super.menuSelectionChanged(isIncluded); if(isIncluded && !wasMouse){ - String menuType = " menu"; + String menuType = resources.getString("menufactory.menu"); if(getMenuBar().getComponentIndex(this) == -1){ - menuType = " sub menu"; + menuType = resources.getString("menufactory.submenu"); }; - NarratorFactory.getInstance().speak(getAccessibleContext().getAccessibleName()+menuType); + NarratorFactory.getInstance().speak(getAccessibleContext().getAccessibleName()+ " "+ menuType); } wasMouse = false; } @@ -91,32 +95,13 @@ } 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; - }; + return new SpeechJCheckBoxMenuItem(text); } /* 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); } }; @@ -145,7 +130,7 @@ 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"); + getAccessibleContext().setAccessibleName(accName +" "+ resources.getString("menufactory.3dot")); } } @@ -153,10 +138,10 @@ public void menuSelectionChanged(boolean isIncluded){ super.menuSelectionChanged(isIncluded); if(isIncluded && !wasMouse){ - String disabled = isEnabled() ? "" : ", disabled"; + String disabled = isEnabled() ? "" : resources.getString("menufactory.disabled"); String accelerator = ""; if(getAccelerator() != null){ - accelerator = ", control " + getAccelerator().toString().substring(getAccelerator().toString().lastIndexOf(' ')); + accelerator = resources.getString("menufactory.ctrl")+" "+ getAccelerator().toString().substring(getAccelerator().toString().lastIndexOf(' ')); } NarratorFactory.getInstance().speak(getAccessibleContext().getAccessibleName()+disabled+accelerator); } @@ -180,5 +165,50 @@ private boolean wasMouse; }; + private static class SpeechJCheckBoxMenuItem extends JCheckBoxMenuItem { + public SpeechJCheckBoxMenuItem(String text){ + super(text); + addItemListener(new ItemListener(){ + @Override + public void itemStateChanged(ItemEvent evt) { + int stateChange = evt.getStateChange(); + if(stateChange != ItemEvent.SELECTED && stateChange != ItemEvent.DESELECTED){ + return; + } + if(!itemChangeMouseFlag){ + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString(stateChange == ItemEvent.SELECTED ? "menufactory.selected" : "menufactory.unselected"), + getAccessibleContext().getAccessibleName())); + } + itemChangeMouseFlag = false; + } + }); + } + + @Override + public void menuSelectionChanged(boolean isIncluded){ + super.menuSelectionChanged(isIncluded); + if(isIncluded && !selectionMouseFlag ){ + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString(isSelected() ? "menufactory.selected" : "menufactory.unselected"), + getAccessibleContext().getAccessibleName())); + } + selectionMouseFlag = false; + } + + @Override + public void processMouseEvent(MouseEvent e){ + selectionMouseFlag = true; + itemChangeMouseFlag = true; + super.processMouseEvent(e); + } + + private boolean selectionMouseFlag = false; + private boolean itemChangeMouseFlag = false; + } + private static JMenuBar menuBar; + private static ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechOptionPane.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechOptionPane.java Wed Apr 25 17:09:09 2012 +0100 @@ -23,6 +23,8 @@ import java.awt.Component; import java.awt.Frame; import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; @@ -33,6 +35,8 @@ import java.util.ResourceBundle; import java.util.Set; +import javax.swing.AbstractAction; +import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; @@ -53,16 +57,144 @@ 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; /** * - * 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. + * An option panel made out of an {@code Object} being displayed and to buttons: one for accepting and another one for + * cancelling the option. + * Furthermore, this class 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 a {@link Narrator} instance. + * * */ -public abstract class SpeechOptionPane { +public class SpeechOptionPane { + + /** + * Construct a new {@code SpeechOptionPane} with no title. The title is displayed at the top of the dialog + * that is displayed after a call to {@code showDialog} + */ + public SpeechOptionPane(){ + this(""); + } + + /** + * Construct a new {@code SpeechOptionPane} with no title. The title is displayed at the top of the dialog + * that is displayed after a call to {@code showDialog} + * + * @param title the String to be displayed + */ + public SpeechOptionPane(String title){ + this.title = title; + okButton = new JButton("OK"); + cancelButton = new JButton("Cancel"); + } + + /** + * Pops the a dialog holding this SpeechOptionPane + * + * @param parent the parent component of the dialog + * @param the {@code Object} to display + * @return an integer indicating the option selected by the user + */ + @SuppressWarnings("serial") + public int showDialog(Component parent,final Object message){ + optPane = new JOptionPane(); + optPane.setMessage(message); + /* Enter will entail a unique action, regardless the component that's focused */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "closeDialog"); + optPane.getActionMap().put("closeDialog", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + okButton.doClick(); + } + }); + optPane.setMessageType(JOptionPane.PLAIN_MESSAGE); + Object[] options = { + okButton, + 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()); + final JDialog dialog = optPane.createDialog(parent, title); + SpeechUtilities.changeTabListener(optPane,dialog); + /* when either button is pressed, dialog is disposed and the button itself becomes the optPane.value */ + ActionListener buttonListener = new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + onClose(dialog,message,(JButton)evt.getSource()); + } + }; + okButton.addActionListener(buttonListener); + cancelButton.addActionListener(buttonListener); + + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + dialog.dispose(); + if(okButton.equals(optPane.getValue())){ + return OK_OPTION; + }else{ + return CANCEL_OPTION; + } + } + + /** + * Sets the string appearing at the top of the dialog where this option pane is displayed when {@code showDialog} + * is called. + * @param title + */ + public void setDialogTitle(String title){ + this.title = title; + } + + /** + * Returns the {@code JButton} that the user has to press (when the option pane is displayed after + * {@code showDialog} is called) in order to accept the option. + * + * @return a reference to the internal {@code JButton} + */ + public JButton getOkButton(){ + return okButton; + } + + /** + * Returns the {@code JButton} that the user has to press (when the option pane is displayed after + * {@code showDialog} is called) in order to reject the option. + * + * @return a reference to the internal {@code JButton} + */ + public JButton getCancelButton(){ + return cancelButton; + } + + /** + * This method is called just after the user pressed either button of the dialog displayed + * after {@code showDialog} is called. + * It assign a value to the return value and it frees the dialog resources. + * It can be overwritten by subclasses but care should be taken of calling this class method via + * {@code super} in order to properly close the dialog. + * + * @param dialog the dialog displayed after {@code showDialog} is called. + * @param message + * @param source the button that triggered the closing of {@code dialog} + */ + protected void onClose(JDialog dialog,Object message, JButton source){ + optPane.setValue(source); + dialog.dispose(); + } + + private String title; + private JOptionPane optPane; + private JButton okButton; + private JButton cancelButton; + + + /* -------- STATIC METHODS ----------- */ public static String showTextAreaDialog(Component parentComponent, String message, String initialSelectionValue){ JTextArea textArea = new JTextArea(NOTES_TEXT_AREA_ROW_SIZE,NOTES_TEXT_AREA_COL_SIZE); @@ -254,8 +386,7 @@ } public static <T,V> int showProgressDialog(Component parentComponent, String message,final ProgressDialogWorker<T,V> worker, int millisToDecideToPopup){ - JProgressBar progressBar = new JProgressBar(); - progressBar.setIndeterminate(true); + JProgressBar progressBar = worker.bar; Object displayObjects[] = {message, progressBar}; final JOptionPane optPane = new JOptionPane(displayObjects); optPane.setOptionType(DEFAULT_OPTION); @@ -363,17 +494,23 @@ public static abstract class ProgressDialogWorker<T,V> extends SwingWorker<T,V> { + public ProgressDialogWorker(){ + bar = new JProgressBar(); + bar.setIndeterminate(true); + } + private void setDialog(JDialog dialog){ this.dialog = dialog; } @Override - protected void done() { + protected void done() { //executed in EDT when the work is done if(dialog != null) dialog.dispose(); } private JDialog dialog; + protected JProgressBar bar; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/TemplateEditor.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/TemplateEditor.java Wed Apr 25 17:09:09 2012 +0100 @@ -23,16 +23,56 @@ 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. + * A template editor is used to create new types of diagrams. + * + * Template editors are run in the Event Dispatching Thread and can therefore make use of swimg components + * to prompt the user with choices about the diagram to be created. The diagram created by + * a template editor is precisely a prototype. + * Such prototypes diagrams will then be used + * to create new 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 { + + /** + * Creates a new {@code Diagram} + * + * @param frame the frame where the template editor is run + * @param existingTemplates the names of already existing templates. The creation + * of a new {@code Diagram} with an already existing name must be prevented in order + * to keep the consistency of the diagram templates. + * + * @return a new {@code Diagram} prototype + */ public Diagram createNew(Frame frame, Collection<String> existingTemplates); + /** + * Edits an existing {@code Diagram} prototype. + * + * @param frame the frame where the template editor is run + * @param existingTemplates the names of already existing templates. The creation + * of a new {@code Diagram} with an already existing name must be prevented in order + * to keep the consistency of the diagram templates. + * @param diagram the diagram to edit + * @return a changed version of {@code diagram} + */ public Diagram edit(Frame frame, Collection<String> existingTemplates, Diagram diagram); + /** + * Templates editor methods are going to be called by the user via a menu item. This method + * returns the label {@code createNew} menu item. + * + * @return a label for the menu item which triggers the creation of a new template through this + * template editor + */ public String getLabelForNew(); + /** + * Templates editor methods are going to be called by the user via a menu item. This method + * returns the label {@code edit} menu item. + * + * @return a label for the menu item which triggers the editing of a new template through this + * template editor + */ public String getLabelForEdit(); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessFilter.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,159 @@ +/* + 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.awareness; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.tree.TreePath; + +import uk.ac.qmul.eecs.ccmi.checkboxtree.CheckBoxTree; +import uk.ac.qmul.eecs.ccmi.checkboxtree.CheckBoxTreeNode; +import uk.ac.qmul.eecs.ccmi.checkboxtree.SetProperties; +import uk.ac.qmul.eecs.ccmi.gui.SpeechOptionPane; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.TreeSonifier; +import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; + +public abstract class AwarenessFilter { + + AwarenessFilter(String propertiesFilePath, String propertiesFileName) throws IOException{ + if(propertiesFilePath == null) + throw new IOException(ResourceBundle.getBundle(AwarenessFilter.class.getName()).getString("error.no_properties_dir")); + properties = new SetProperties(); + propertiesFile = new File(propertiesFilePath,propertiesFileName); + if(propertiesFile.exists()) + properties.load(propertiesFile); + } + + /** + * Returns the file name this instance of AwarenessFilter was build from. It should return an existing + * XML file name, which will be read through {@code getClass().getResourceAsStream}, representing the tree + * displayed after calling {@code showDialog}. + * + * @see {@link uk.ac.qmul.eecs.ccmi.checkboxtree.CheckBoxTree} + * + * @return the name of the XML file + */ + protected abstract String getXMLFileName(); + + protected abstract String getDialogTitle(); + + public abstract void saveProperties(Component parentComponent); + + public void showDialog(Component parent){ + /* create and init components */ + InputStream in = getClass().getResourceAsStream(getXMLFileName()); + /* build the tree with a copy of the current properties, so that they won't be * + * affected if the user press cancel be affected if the user */ + CheckBoxTree tree = null; + synchronized(properties.getMonitor()){ + tree = new CheckBoxTree(in,new SetProperties(properties)); + } + try{ + in.close(); + }catch(IOException ioe){ + ioe.printStackTrace(); + } + new TreeSonifier(){ + @Override + protected String currentPathSpeech(JTree tree) { + TreePath path = tree.getSelectionPath(); + CheckBoxTreeNode selectedPathTreeNode = (CheckBoxTreeNode)path.getLastPathComponent(); + return selectedPathTreeNode.spokenText(); + } + @Override + protected void space(JTree tree){ + TreePath path = tree.getSelectionPath(); + CheckBoxTreeNode treeNode = (CheckBoxTreeNode)path.getLastPathComponent(); + ((CheckBoxTree)tree).toggleSelection(treeNode); + NarratorFactory.getInstance().speak(treeNode.spokenText()); + } + }.sonify(tree); + + /* make the user aware that the dialog is opening */ + NarratorFactory.getInstance().speak(getDialogTitle()); + + SpeechOptionPane optionPane = new SpeechOptionPane(getDialogTitle()); + JPanel panel = new JPanel(new GridBagLayout()); + GridBagUtilities gridBag = new GridBagUtilities(); + JScrollPane scrollPane = new JScrollPane(tree); + scrollPane.setPreferredSize(new Dimension(200,300)); + panel.add(scrollPane,gridBag.all()); + + int result = optionPane.showDialog(parent, panel); + if(result == SpeechOptionPane.CANCEL_OPTION) + return; + + synchronized(properties.getMonitor()){ + configurationHasChanged = true; + properties.clear(); + for(String property : tree.getProperties()) + properties.add(property); + } + } + + /** + * This method can be called to query the filter on whether the configuration has changed, + * that is {@code showDialog} has been called since the last time this method was called. + * Successive calls of this method will return false until the configuration dialog will be + * displayed again. This method is thread-safe and can be called by thread different from + * the Event Dispatching Thread, where {@code showDialog} should be called. + * + * @return true if the configuration has changed + */ + public boolean configurationHasChanged(){ + synchronized(properties.getMonitor()){ + boolean toReturn = configurationHasChanged; + if(configurationHasChanged) + configurationHasChanged = false; + return toReturn; + } + } + + public void saveProperties(Component parentComponent, String comments) { + ResourceBundle resources = ResourceBundle.getBundle(AwarenessFilter.class.getName()); + try { + if(!propertiesFile.getParentFile().exists()) + throw new IOException(resources.getString("error.no_properties_dir")); + propertiesFile.createNewFile(); + properties.store(propertiesFile, comments); + }catch (IOException ioe){ + SpeechOptionPane.showMessageDialog( + parentComponent, + MessageFormat.format(resources.getString("error.write_file"), + ioe.getLocalizedMessage()) + ); + } + } + + protected final SetProperties properties; + private boolean configurationHasChanged; + private File propertiesFile; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessFilter.properties Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,36 @@ +tree_root=Awareness Filter +broadcast.properties.file_name=broadcast.txt +broadcast.properties.comments=Awareness message broadcast configuration file. Generated automatically. DO NOT EDIT! + +display.properties.file_name=display.txt +display.properties.comments=Awareness message display configuration file. Generated automatically. DO NOT EDIT! + +error.no_properties_dir=Could not open the library directory ("ccmi_editor_data/libs") +error.write_file=Error while saving awareness configuration: {0} + +dialog.display.title=Display Filter Dialog +dialog.broadcast.title=Broadcast Filter Dialog + + +action.text.add_node=added node{0} +action.text.add_edge=added edge{0} +action.text.remove_node=removing node{0} +action.text.remove_edge=removing edge{0} +action.text.edit_node=editing node{0} +action.text.edit_edge=editing edge{0} +action.text.move_node=moving node{0} +action.text.move_edge=moving edge{0} +action.text.select_node=selected node{0} +action.text.unselect_node=unselected node{0} + +action.text.add_node.verb=added node{0} +action.text.add_edge.verb=added edge{0} +action.text.remove_node.verb=is removing node{0} +action.text.remove_edge.verb=is removing edge{0} +action.text.edit_node.verb=is editing node{0} +action.text.edit_edge.verb=is editing edge{0} +action.text.move_node.verb=is moving node{0} +action.text.move_edge.verb=is moving edge{0} +action.text.select_node.verb=selected node{0} +action.text.unselect_node.verb=unselected node{0} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessPanel.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,61 @@ +/* + 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.awareness; + +import javax.swing.JSplitPane; + +/** + * The panel where awareness informations are displayed. The panel is split in two sub panel: the top + * sub panel holds informations about the actions of other users, the bottom sub panel holds a list + * of the name of the users currently partaking the collaboration. + * + */ +@SuppressWarnings("serial") +public class AwarenessPanel extends JSplitPane { + /** + * Creates a new instance of this class, bound to a diagram. + * @param diagramName the name of the diagram this panel is bound to. + */ + public AwarenessPanel(String diagramName){ + super(VERTICAL_SPLIT,true); + usersPane = new AwarenessTextPane("user names panel"); + recordsPane = new AwarenessTextPane("awareness panel"); + setTopComponent(recordsPane); + setRightComponent(usersPane); + setResizeWeight(1.0); + setDividerLocation(0.4); + this.diagramName = diagramName; + } + + public AwarenessTextPane getUsersPane() { + return usersPane; + } + + public AwarenessTextPane getRecordsPane() { + return recordsPane; + } + + public String getDiagramName(){ + return diagramName; + } + + private AwarenessTextPane usersPane; + private AwarenessTextPane recordsPane; + private String diagramName; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessPanelEditor.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,120 @@ +/* + 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.awareness; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.Timer; + +import uk.ac.qmul.eecs.ccmi.network.AwarenessMessage; + +public class AwarenessPanelEditor { + public AwarenessPanelEditor() { + awarenessPanels = Collections.synchronizedList(new ArrayList<AwarenessPanel>()); + } + + public void addAwarenessPanel(AwarenessPanel panel){ + awarenessPanels.add(panel); + } + + public void removeAwarenessPanel(AwarenessPanel panel){ + awarenessPanels.remove(panel); + } + + /** + * Replaces a user's user name with a new one. + * + * @param diagramName the diagram the update has to be performed on + * @param userNames a concatenation of the new user name and the old one. The + * old user name can possibly be the empty string if the client has sent its + * user name for the first time (hence no old user name exists). + */ + public void replaceUserName(String diagramName, String userNames){ + AwarenessPanel panel = findPanel(diagramName); + if(panel == null) + return; + String [] names = userNames.split(AwarenessMessage.USERNAMES_SEPARATOR);// [0] = new name, [1] = old name + if(names.length == 2){ + if(names[0].isEmpty()){ // if the new name is empty, then it's like just removing the old one + panel.getUsersPane().remove(names[1]+'\n'); + return; + } + panel.getUsersPane().remove(names[1]+'\n'); + } + panel.getUsersPane().insert(names[0]+'\n'); + } + + public void removeUserName(String diagramName, String userName){ + AwarenessPanel panel = findPanel(diagramName); + if(panel != null) + panel.getUsersPane().remove(userName+'\n'); + } + + public void addRecord(String diagramName, String record){ + AwarenessPanel panel = findPanel(diagramName); + if(panel == null) + return; + panel.getRecordsPane().insert(record); + } + + public void addTimedRecord(final String diagramName, final String record){ + addRecord(diagramName, record); + Timer timer = new Timer(TIMER_DELAY,new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + removeRecord(diagramName,record); + } + }); + timer.setRepeats(false); + timer.start(); + } + + public void removeRecord(String diagramName, String record){ + AwarenessPanel panel = findPanel(diagramName); + if(panel == null) + return; + panel.getRecordsPane().remove(record); + } + + public void clearRecords(String diagramName){ + AwarenessPanel panel = findPanel(diagramName); + if(panel == null) + return; + panel.getRecordsPane().clear(); + } + + private AwarenessPanel findPanel(String diagramName){ + // it's a synchronized collection, this will synchronize with add and remove + synchronized(awarenessPanels){ + for(AwarenessPanel p : awarenessPanels){ + if(p.getDiagramName().equals(diagramName)){ + return p; + } + } + } + return null; + } + + private List<AwarenessPanel> awarenessPanels; + private static int TIMER_DELAY = 2000; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessTextPane.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,82 @@ +/* + 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.awareness; + +import java.awt.Component; +import java.awt.event.KeyEvent; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.JTextPane; +import javax.swing.KeyStroke; +import javax.swing.text.DefaultEditorKit; + +import uk.ac.qmul.eecs.ccmi.main.DiagramEditorApp; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +@SuppressWarnings("serial") +public class AwarenessTextPane extends JTextPane { + public AwarenessTextPane(String accessibleName){ + records = new LinkedList<String>(); + addKeyListener(SpeechUtilities.getSpeechKeyListener(false,true)); + /* prevents getText() from automatically turn all the \n to \r\n */ + getDocument().putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n"); + getAccessibleContext().setAccessibleName(accessibleName); + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,0), "none"); + SpeechUtilities.changeTabListener(this,DiagramEditorApp.getFrame()); + } + + public void insert(String record){ + records.add(0, record); + update(); + } + + public void remove(String record){ + records.remove(record); + update(); + } + + public void clear(){ + records.clear(); + update(); + } + + protected void processKeyEvent(KeyEvent e){ + if(e.getKeyCode() != KeyEvent.VK_UP && + e.getKeyCode() != KeyEvent.VK_DOWN && + e.getKeyCode() != KeyEvent.VK_RIGHT && + e.getKeyCode() != KeyEvent.VK_LEFT && + e.getKeyCode() != KeyEvent.VK_TAB) + return; + super.processKeyEvent(new KeyEvent((Component)e.getSource(),e.getID(), + e.getWhen(),0,e.getKeyCode(),e.getKeyChar())); + } + + private void update(){ + StringBuilder builder = new StringBuilder(); + for(String r : records){ + builder.append(r); + } + selectAll(); + replaceSelection(builder.toString()); + } + + private List<String> records; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/BroadcastFilter.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,158 @@ +/* + 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.awareness; + +import java.awt.Component; +import java.io.IOException; +import java.util.ResourceBundle; + +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +public class BroadcastFilter extends AwarenessFilter { + public static BroadcastFilter getInstance(){ + return broadcastFilter; + } + + public static BroadcastFilter createInstance() throws IOException { + broadcastFilter = new BroadcastFilter(); + return broadcastFilter; + } + + private BroadcastFilter() throws IOException{ + super(PreferencesService.getInstance().get("dir.libs", null), + ResourceBundle.getBundle(AwarenessFilter.class.getName()).getString("broadcast.properties.file_name")); + } + + @Override + protected String getXMLFileName(){ + return "BroadcastFilterTree.xml"; + } + + @Override + protected String getDialogTitle(){ + return ResourceBundle.getBundle(AwarenessFilter.class.getName()).getString("dialog.broadcast.title"); + } + + @Override + public void saveProperties(Component parentComponent){ + ResourceBundle resources = ResourceBundle.getBundle(AwarenessFilter.class.getName()); + super.saveProperties( + parentComponent, + resources.getString("broadcast.properties.comments") + ); + } + + public boolean accept(DiagramEventActionSource action){ + /* don't accept if the user didn't select the modality this action was generated from */ + switch(action.type){ + case TREE : if(!properties.contains("Broadcast Filter.Where (Source).Audio View")) return false; + break; + case GRPH : if(!properties.contains("Broadcast Filter.Where (Source).Graphic View")) return false; + break; + case HAPT : if(!properties.contains("Broadcast Filter.Where (Source).Haptic View")) return false; + break; + } + + switch(action.getCmd()){ + case INSERT_EDGE : + case SELECT_NODE_FOR_EDGE_CREATION : + case UNSELECT_NODE_FOR_EDGE_CREATION : if(!properties.contains("Broadcast Filter.What (Action).Edge Add")) return false; + break; + case REMOVE_EDGE : if(!properties.contains("Broadcast Filter.What (Action).Edge Remove")) return false; + break; + case INSERT_NODE : if(!properties.contains("Broadcast Filter.What (Action).Node Add")) return false; + break; + case REMOVE_NODE : if(!properties.contains("Broadcast Filter.What (Action).Node Remove")) return false; + break; + case SET_NODE_NAME : + case SET_PROPERTY : + case SET_PROPERTIES : + case ADD_PROPERTY : + case REMOVE_PROPERTY : + case SET_MODIFIERS : + if(!properties.contains("Broadcast Filter.What (Action).Node Edited")) return false; + break; + case SET_ENDDESCRIPTION : + case SET_EDGE_NAME : + case SET_ENDLABEL : + if(!properties.contains("Broadcast Filter.What (Action).Edge Edited")) return false; + break; + case STOP_NODE_MOVE : + case TRANSLATE_NODE : + if(!properties.contains("Broadcast Filter.What (Action).Node Moved")) return false; + break; + case TRANSLATE_EDGE : + case BEND : + case STOP_EDGE_MOVE : + if(!properties.contains("Broadcast Filter.What (Action).Edge Moved")) return false; + break; + default : return false; // if it's none of these commands(e.g. set notes), then don't accept the action + } + return true; + } + + public DiagramEventActionSource process(DiagramEventActionSource action){ + /* delete the timestamp if the user decided not to broadcast it */ + if(properties.contains("Broadcast Filter.When.Active History")) + action.setTimestamp(System.currentTimeMillis()); + else + action.setTimestamp(0); + + /* delete the user id if the user decided not to broadcast it */ + if(!properties.contains("Broadcast Filter.Who.User Id")) + action.setUserName(""); + + /* delete the element id if the user decided not to broadcast it */ + switch(action.getCmd()){ + case INSERT_NODE : + case REMOVE_NODE : + case TRANSLATE_NODE : + case SET_NODE_NAME : + case STOP_NODE_MOVE : + case SET_PROPERTY : + case SET_PROPERTIES : + case ADD_PROPERTY : + case REMOVE_PROPERTY : + case SET_MODIFIERS : + if(!properties.contains("Broadcast Filter.What (Object).Which Node")) + action.setElementID(Node.NO_ID); + break; + case INSERT_EDGE : + case SELECT_NODE_FOR_EDGE_CREATION : + case UNSELECT_NODE_FOR_EDGE_CREATION : + case REMOVE_EDGE : + case SET_ENDDESCRIPTION : + case SET_ENDLABEL : + case SET_EDGE_NAME : + case TRANSLATE_EDGE : + case BEND : + case STOP_EDGE_MOVE : + if(!properties.contains("Broadcast Filter.What (Object).Which Edge")) + action.setElementID(Edge.NO_ID); + break; + } + return action; + } + + private static BroadcastFilter broadcastFilter; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/BroadcastFilterTree.xml Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> + +<unselectable value="Broadcast Filter"> + + <selectable value="Who"> + <selectable value="User Id"/> + </selectable> + + <selectable value="What (Action)"> + <selectable value="Node Add"/> + <selectable value="Node Remove"/> + <selectable value="Node Edited"/> + <selectable value="Node Moved"/> + <selectable value="Edge Add"/> + <selectable value="Edge Remove"/> + <selectable value="Edge Edited"/> + <selectable value="Edge Moved"/> + </selectable> + + <selectable value="What (Object)"> + <selectable value="Which Node"/> + <selectable value="Which Edge"/> + </selectable> + + <selectable value="Where (Object)"> + <selectable value="Edge Coordinates" /> + </selectable> + + <selectable value="Where (Source)"> + <selectable value="Audio View"/> + <selectable value="Haptic View"/> + <selectable value="Graphic View"/> + </selectable> + + <selectable value="When"> + <selectable value="Active History"/> + </selectable> + +</unselectable> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/DisplayFilter.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,159 @@ +/* + 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.awareness; + +import java.awt.Component; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.network.Command.Name; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + + +public class DisplayFilter extends AwarenessFilter { + public static DisplayFilter createInstance() throws IOException{ + if(displayFilter == null) + displayFilter = new DisplayFilter(); + return displayFilter; + } + + public static DisplayFilter getInstance(){ + return displayFilter; + } + + private DisplayFilter() throws IOException{ + super(PreferencesService.getInstance().get("dir.libs", null), + ResourceBundle.getBundle(AwarenessFilter.class.getName()).getString("display.properties.file_name")); + resources = ResourceBundle.getBundle(AwarenessFilter.class.getName()); + } + + @Override + protected String getXMLFileName() { + return "DisplayFilterTree.xml"; + } + + @Override + protected String getDialogTitle(){ + return resources.getString("dialog.display.title"); + } + + @Override + public void saveProperties(Component parentComponent) { + super.saveProperties(parentComponent, resources.getString("display.properties.comments")); + } + + public String processForSpeech(DiagramEventActionSource actionSource){ + return process(actionSource,"Speech"); + } + + public String processForText(DiagramEventActionSource actionSource){ + return process(actionSource,"Text"); + } + + private String process(DiagramEventActionSource actionSource,String propertiesAppendix){ + boolean includeUser = properties.contains("Display Filter.Who."+propertiesAppendix); + boolean includeAction = properties.contains("Display Filter.What (Action)."+propertiesAppendix); + boolean includeObject = properties.contains("Display Filter.What (Object)."+propertiesAppendix); + + // match with server configuration + includeUser = (includeUser && (!actionSource.getUserName().isEmpty())); + includeObject = (includeObject && (actionSource.getElementID() != DiagramElement.NO_ID)); + + /* build up the sentence to be displayed to the user */ + StringBuilder builder = new StringBuilder(); + + if(includeUser){ + builder.append(actionSource.getUserName()).append(' '); + } + + /* if the object is included by the filter it will be * + * passed to the message format of the action string */ + String objectString = ""; + if(includeObject){ + objectString = actionSource.getElementName(); + } + + if(includeAction){ + builder.append(getActionString(actionSource.getCmd()," " + objectString,includeUser)); + }else if(!objectString.isEmpty()){ + builder.append(objectString); + } + + if(!(builder.length() == 0)) + builder.append('\n'); + return builder.toString(); + } + + private String getActionString(Name cmd, String objectString, boolean includeVerb) { + String verb = includeVerb ? ".verb" : ""; + switch(cmd){ + case INSERT_EDGE : + return MessageFormat.format(resources.getString("action.text.add_edge"+verb), + objectString); + case INSERT_NODE : + return MessageFormat.format(resources.getString("action.text.add_node"+verb), + objectString); + case REMOVE_NODE : + return MessageFormat.format(resources.getString("action.text.remove_node"+verb), + objectString); + case REMOVE_EDGE : + return MessageFormat.format(resources.getString("action.text.remove_edge"+verb), + objectString); + case SET_NODE_NAME : + case SET_PROPERTY : + case SET_PROPERTIES : + case CLEAR_PROPERTIES : + case SET_NOTES : + case ADD_PROPERTY : + case REMOVE_PROPERTY : + case SET_MODIFIERS : + return MessageFormat.format(resources.getString("action.text.edit_node"+verb), + objectString); + case SET_ENDDESCRIPTION : + case SET_EDGE_NAME : + case SET_ENDLABEL : + return MessageFormat.format(resources.getString("action.text.edit_edge"+verb), + objectString); + case TRANSLATE_NODE : + case STOP_NODE_MOVE : + return MessageFormat.format(resources.getString("action.text.move_node"+verb), + objectString); + case TRANSLATE_EDGE : + case BEND : + case STOP_EDGE_MOVE : + return MessageFormat.format(resources.getString("action.text.move_edge"+verb), + objectString); + case SELECT_NODE_FOR_EDGE_CREATION : + return MessageFormat.format(resources.getString("action.text.select_node"+verb), + objectString); + case UNSELECT_NODE_FOR_EDGE_CREATION : + return MessageFormat.format(resources.getString("action.text.unselect_node"+verb), + objectString); + default : return ""; + } + } + + private static DisplayFilter displayFilter; + ResourceBundle resources; +} + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/DisplayFilterTree.xml Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> + +<unselectable value="Display Filter"> + <selectable value="Who"> + <selectable value="Text"/> + <selectable value="Color"/> + <selectable value="Speech"/> + </selectable> + + <selectable value="What (Action)"> + <selectable value="Text"/> + <selectable value="Speech"/> + </selectable> + + <selectable value="What (Object)"> + <selectable value="Text"/> + <selectable value="Color"/> + <selectable value="Speech"/> + <selectable value="Non speech sound"/> + </selectable> +</unselectable> \ No newline at end of file
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileSystemTree.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileSystemTree.java Wed Apr 25 17:09:09 2012 +0100 @@ -25,6 +25,7 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.io.File; +import java.io.IOException; import java.util.LinkedList; import java.util.ResourceBundle; @@ -76,15 +77,19 @@ public void setSelectionPath(TreePath path){ super.setSelectionPath(path); scrollPathToVisible(path); + getSelectionPath(); } - + public void setSelectionPath(File file){ if(file == null) return; - /* we need the absolute path as the tree has to be expanded from the root to file * - * and we need all the directory names */ - file = file.getAbsoluteFile(); + try { + file = file.getCanonicalFile(); + } catch (IOException e) { + setSelectionPath(new TreePath(getModel().getRoot())); + 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); @@ -120,7 +125,10 @@ treeSelectionListenerGateOpen = false; ((DefaultTreeModel)getModel()).setRoot(FileSystemTreeNode.getRootNode(filter)); treeSelectionListenerGateOpen = true; - setSelectionPath(file); + if(file == null) + setSelectionPath(new TreePath(getModel().getRoot())); + else + setSelectionPath(file); } @Override
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.java Wed Apr 25 17:09:09 2012 +0100 @@ -21,31 +21,22 @@ 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.gui.SpeechOptionPane; import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; @@ -61,22 +52,6 @@ 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() { @@ -104,27 +79,6 @@ 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); @@ -180,48 +134,52 @@ @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); + return showDialog(parent,true); } @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); + return showDialog(parent,false); } - private int showDialog(Component parent){ - optPane = new JOptionPane(); - optPane.setMessage(this); - optPane.setMessageType(JOptionPane.PLAIN_MESSAGE); - Object[] options = { - isOpenFileDialog ? openButton : saveButton, - cancelButton + private int showDialog(Component parent, boolean isOpenFileDialog){ + /* overrides on close so that, before closing the dialog it checks that a file name has actually * + * been entered by the user. If not, the dialog won't close and a error will be notified through the narrator */ + SpeechOptionPane optionPane = new SpeechOptionPane(resources.getString("dialog.open.title")){ + @Override + protected void onClose(JDialog dialog,Object message, JButton source){ + if(source.equals(getOkButton())){ + if(fileNameTextField.getText().isEmpty()){ + NarratorFactory.getInstance().speak(resources.getString("dialog.error.no_file_name")); + return; + } + } + super.onClose(dialog, message, source); + } }; - 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 */ + + if(isOpenFileDialog) + optionPane.getOkButton().setText(resources.getString("open_button.label")); + else + optionPane.getOkButton().setText(resources.getString("save_button.label")); + optionPane.getCancelButton().setText(resources.getString("cancel_button.label")); + + /* add the speech listener just before showing up and then remove it, 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); + int result = optionPane.showDialog(parent, this); fileTypeComboBox.removeItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); - if((isOpenFileDialog && openButton.equals(optPane.getValue()))||(!isOpenFileDialog && saveButton.equals(optPane.getValue()))){ + + if(result == SpeechOptionPane.OK_OPTION){ 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 */ @@ -245,12 +203,6 @@ 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 {
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.properties Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.properties Wed Apr 25 17:09:09 2012 +0100 @@ -3,14 +3,14 @@ file={0} tree_root.label=File System -tree.accessible_name=File System +tree.accessible_name=File System Tree open_button.label=Open cancel_button.label=Cancel save_button.label=Save -dialog.open.title=Open File -dialog.save.title=Save File +dialog.open.title=Open File Dialog +dialog.save.title=Save File Dialog dialog.open.message=Open File dialog. {0} selected dialog.save.message=Save File dialog. {0} selected dialog.file_chooser.file_name=File Name:
--- a/java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PersistenceManager.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PersistenceManager.java Wed Apr 25 17:09:09 2012 +0100 @@ -52,9 +52,10 @@ 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.DiagramTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel; import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; import uk.ac.qmul.eecs.ccmi.gui.Edge; import uk.ac.qmul.eecs.ccmi.gui.Node; import uk.ac.qmul.eecs.ccmi.utils.CharEscaper; @@ -202,10 +203,10 @@ /* store notes */ Element notesTag = doc.createElement(NOTES); - DiagramModelTreeNode treeRoot = (DiagramModelTreeNode)diagram.getTreeModel().getRoot(); + DiagramTreeNode treeRoot = (DiagramTreeNode)diagram.getTreeModel().getRoot(); for( @SuppressWarnings("unchecked") - Enumeration<DiagramModelTreeNode> enumeration = treeRoot.depthFirstEnumeration(); enumeration.hasMoreElements();){ - DiagramModelTreeNode treeNode = enumeration.nextElement(); + Enumeration<DiagramTreeNode> enumeration = treeRoot.depthFirstEnumeration(); enumeration.hasMoreElements();){ + DiagramTreeNode treeNode = enumeration.nextElement(); if(!treeNode.getNotes().isEmpty()){ Element noteTag = doc.createElement(NOTE); Element treeNodeTag = doc.createElement(TREE_NODE); @@ -322,6 +323,7 @@ CollectionModel<Node,Edge> collectionModel = diagram.getCollectionModel(); TreeModel<Node,Edge> treeModel = diagram.getTreeModel(); + /* a map linking node ids in the XML file to the actual Node object they represent */ Map<String,Node> nodesId = new LinkedHashMap<String,Node>(); if(doc.getElementsByTagName(COMPONENTS).item(0) == null) @@ -360,7 +362,7 @@ }catch(NumberFormatException nfe){ throw new IOException(resources.getString("dialog.error.malformed_file"),nfe); } - collectionModel.insert(node); + collectionModel.insert(node,DiagramEventSource.PERS); try{ node.decode(doc, nodeTag); }catch(IOException ioe){ // just give a message to the exception @@ -397,7 +399,7 @@ }catch(IOException ioe){ throw new IOException(resources.getString("dialog.error.malformed_file"),ioe); } - collectionModel.insert(edge); + collectionModel.insert(edge,DiagramEventSource.PERS); } /* retrieve bookmarks */ @@ -408,8 +410,8 @@ 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); + DiagramTreeNode treeNode = getTreeNodeFromString(treeModel,path); + treeModel.putBookmark(key, treeNode,DiagramEventSource.PERS); } /* retrieve notes */ @@ -424,8 +426,8 @@ 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); + DiagramTreeNode treeNode = getTreeNodeFromString(treeModel,path); + treeModel.setNotes(treeNode,content,DiagramEventSource.PERS); } /* normally nodes and edges should be saved in order, this is to prevent * @@ -492,7 +494,7 @@ return eList; } - private static String getTreeNodeAsString(DiagramModelTreeNode treeNode){ + private static String getTreeNodeAsString(DiagramTreeNode treeNode){ TreeNode[] path = treeNode.getPath(); StringBuilder builder = new StringBuilder(); for(int i=0;i<path.length-1; i++) @@ -503,15 +505,15 @@ } - private static DiagramModelTreeNode getTreeNodeFromString(TreeModel<Node,Edge> model, String path) throws IOException{ - DiagramModelTreeNode treeNode = (DiagramModelTreeNode)model.getRoot(); + private static DiagramTreeNode getTreeNodeFromString(TreeModel<Node,Edge> model, String path) throws IOException{ + DiagramTreeNode treeNode = (DiagramTreeNode)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)); + treeNode = (DiagramTreeNode) treeNode.getChildAt(Integer.parseInt(nodeAsString)); }catch(Exception e){ throw new IOException(e); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/haptics/DummyHaptics.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/DummyHaptics.java Wed Apr 25 17:09:09 2012 +0100 @@ -34,39 +34,42 @@ public int init(int width, int height) { return 0;} @Override - public synchronized void addNode(double x, double y, int diagramId) {} + public synchronized void addNode(double x, double y, int nodeHashCode, String diagramName) {} @Override - public synchronized void removeNode(int diagramId) {} + public synchronized void removeNode(int nodeHashCode, String diagramName) {} @Override - public synchronized void removeEdge(int diagramId){} + public synchronized void removeEdge(int nodeHashCode, String diagramName){} @Override public synchronized void dispose() {} @Override - public void addNewDiagram(int id, boolean switchAfter) {} + public void addNewDiagram(String diagramName, boolean switchAfter) {} @Override - public synchronized void switchDiagram(int id) { } + public synchronized void switchDiagram(String diagramName) { } @Override - public synchronized void removeDiagram(int idToRemove, Integer idNext) {} + public synchronized void removeDiagram(String diagramNameToRemove, String diagramNameNext) {} @Override - public synchronized void moveNode(double x, double y, int diagramId) {} + public synchronized void moveNode(double x, double y, int nodeHashCode, String diagramName) {} @Override - public synchronized void addEdge(int diagramId, double[] xs, double[] ys, - BitSet[] adjMatrix, int nodeStart, int stipplePattern, Line2D attractLine) {} + public synchronized void addEdge(int nodeHashCode, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, int stipplePattern, Line2D attractLine, String diagramName) {} @Override - public synchronized void updateEdge(int diagramId, double[] xs, - double[] ys, BitSet[] adjMatrix, int nodeStart, Line2D attractLine) {} + public synchronized void updateEdge(int nodeHashCode, double[] xs, + double[] ys, BitSet[] adjMatrix, int nodeStart, Line2D attractLine, String diagramName) {} @Override - public synchronized void attractTo(int diagramId) {} + public synchronized void attractTo(int elementHashCode) {} + + @Override + public void pickUp(int elementHashCode){} @Override public boolean isAlive(){
--- a/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListenerCommand.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListenerCommand.java Wed Apr 25 17:09:09 2012 +0100 @@ -32,6 +32,7 @@ MOVE, INFO, NONE, + PICK_UP, ERROR; public static HapticListenerCommand fromChar(char c){ @@ -44,6 +45,7 @@ case 'u' : return UNSELECT; case 'g' : return PLAY_SOUND; case 'e' : return ERROR; + case 'c' : return PICK_UP; default : return NONE; } } @@ -59,7 +61,6 @@ switch(i){ case 0 : return MAGNET_OFF; case 1 : return MAGNET_ON; - case 2 : return HOOK_ON; case 3 : return DRAG; default : return NONE; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/haptics/Haptics.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Haptics.java Wed Apr 25 17:09:09 2012 +0100 @@ -27,31 +27,33 @@ public int init(int width, int height) throws IOException; - public void addNewDiagram(int id, boolean switchAfter); + public void addNewDiagram(String diagramName, boolean switchAfter); - public void switchDiagram(int id); + public void switchDiagram(String diagramName); - public void removeDiagram(int idToRemove, Integer idNext); + public void removeDiagram(String diagramNameToRemove, String diagramNameOfNext); - public void addNode(double x, double y, int diagramId); + public void addNode(double x, double y, int nodeHashCode, String diagramName); - public void removeNode(int diagramId); + public void removeNode(int nodeHashCode, String diagramName); - public void moveNode(double x, double y, int diagramId); + public void moveNode(double x, double y, int nodeHashCode, String diagramName); - public void addEdge(int diagramId, double[] xs, double[] ys, + public void addEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart, int stipplePattern, - Line2D attractLine); + Line2D attractLine, String diagramName); - public void updateEdge(int diagramId, double[] xs, double[] ys, - BitSet[] adjMatrix, int nodeStart, Line2D attractLine); + public void updateEdge(int edgeHashCode, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, Line2D attractLine, String diagramName); - public void removeEdge(int diagramId); + public void removeEdge(int edgeHashCode, String diagramName); - public void attractTo(int diagramId); + public void attractTo(int elementHashCode); + + public void pickUp(int elementHashCode); public boolean isAlive(); public void dispose(); - + }
--- a/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticsFactory.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticsFactory.java Wed Apr 25 17:09:09 2012 +0100 @@ -27,10 +27,18 @@ * */ public class HapticsFactory { + /** + * Creates a new instance of {@code Haptics}. If an Omni Haptic can be successfully initialised + * {@code Haptics} will handle the device, otherwise all the calls to the object will have no effect. + * + * @param listener an haptic commands listener to link to the {@code Haptics} instance. + */ public static void createInstance(HapticListener listener) { if(hapticsInstance != null) throw new IllegalStateException("create instance must be called once only"); - hapticsInstance = new DummyHaptics(); + hapticsInstance = OmniHaptics.createInstance(listener); + if(hapticsInstance == null) + hapticsInstance = new DummyHaptics(); } public static Haptics getInstance(){
--- a/java/src/uk/ac/qmul/eecs/ccmi/haptics/Node.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Node.java Wed Apr 25 17:09:09 2012 +0100 @@ -46,5 +46,4 @@ 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/haptics/OmniHaptics.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,431 @@ +/* + 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.Dimension; +import java.awt.Toolkit; +import java.awt.geom.Line2D; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; +import java.util.ListIterator; + +import uk.ac.qmul.eecs.ccmi.utils.ResourceFileWriter; +import uk.ac.qmul.eecs.ccmi.utils.OsDetector; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/* + * + * The implementation of the Haptics interface which uses Sensable® + * PHANTOM Omni® haptic device. + * + */ +class OmniHaptics extends Thread implements Haptics { + + static Haptics createInstance(HapticListener listener) { + if(listener == null) + throw new IllegalArgumentException("listener cannot be null"); + + if(OsDetector.isWindows()){ + /* try to load .dll. First copy it in the home/ccmi_editor_data/lib directory */ + URL url = OmniHaptics.class.getResource("Haptics.dll"); + ResourceFileWriter fileWriter = new ResourceFileWriter(url); + fileWriter.writeToDisk( + PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")), + "Haptics.dll"); + String path = fileWriter.getFilePath(); + if(path == null) + return null; + try{ + System.load( path ); + }catch(UnsatisfiedLinkError e){ + return null; + } + }else{ + return null; + } + + OmniHaptics omniHaptics = new OmniHaptics("Haptics"); + omniHaptics.hapticListener = listener; + /* start up the listener which immediately stops, waiting for commands */ + if(!omniHaptics.hapticListener.isAlive()) + omniHaptics.hapticListener.start(); + /* start up the haptics thread which issues commands from the java to the c++ thread */ + omniHaptics.start(); + /* wait for the haptics thread (now running native code) to initialize (need to know if initialization is successful) */ + synchronized(omniHaptics){ + try { + omniHaptics.wait(); + }catch (InterruptedException ie) { + throw new RuntimeException(ie); // must never happen + } + } + if(omniHaptics.hapticInitFailed){ + /* the initialization has failed, the haptic thread is about to die */ + while(!omniHaptics.hapticListener.isInterrupted()){ + omniHaptics.hapticListener.interrupt(); + } + omniHaptics.hapticListener = null; //leave the listener to the GC + return null; + }else{ + return omniHaptics; + } + } + + private OmniHaptics(String threadName){ + super(threadName); + /* get the screen size which will be passed to init methos in order to set up a window + * for the haptic with the same size as the swing one + */ + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + + int screenWidth = (int)screenSize.getWidth(); + int screenHeight = (int)screenSize.getHeight(); + + width = screenWidth * 5 / 8; + height = screenHeight * 5 / 8; + + newHapticId = false; + dumpHapticId = false; + shutdown = false; + hapticInitFailed = false; + nodes = new HashMap<String,ArrayList<Node>>(); + edges = new HashMap<String,ArrayList<Edge>>(); + currentNodes = EMPTY_NODE_LIST; + currentEdges = EMPTY_EDGE_LIST; + nodesMaps = new HashMap<String,HashMap<Integer,Node>>(); + edgesMaps = new HashMap<String,HashMap<Integer,Edge>>(); + currentNodesMap = EMPTY_NODE_MAP; + currentEdgesMap = EMPTY_EDGE_MAP; + } + + + @Override + public native int init(int width, int height) throws IOException; + + @Override + public void addNewDiagram(String name, boolean switchAfter){ + ArrayList<Node> cNodes = new ArrayList<Node>(30); + ArrayList<Edge> cEdges = new ArrayList<Edge>(30); + nodes.put(name, cNodes); + edges.put(name, cEdges); + + HashMap<Integer,Node> cNodesMap = new HashMap<Integer,Node>(); + HashMap<Integer,Edge> cEdgesMap = new HashMap<Integer,Edge>(); + nodesMaps.put(name, cNodesMap); + edgesMaps.put(name, cEdgesMap); + + if(switchAfter){ + synchronized(this){ + currentNodes = cNodes; + currentEdges = cEdges; + currentNodesMap = cNodesMap; + currentEdgesMap = cEdgesMap; + } + } + } + + @Override + public synchronized void switchDiagram(String diagramName){ + // check nodes only, as the edges and nodes maps are strongly coupled + if(!nodes.containsKey(diagramName)) + throw new IllegalArgumentException("Diagram " + diagramName + " not present among the current ones"); + + currentNodes = nodes.get(diagramName); + currentEdges = edges.get(diagramName); + currentNodesMap = nodesMaps.get(diagramName); + currentEdgesMap = edgesMaps.get(diagramName); + } + + @Override + public synchronized void removeDiagram(String diagramNameToRemove, String diagramNameNext){ + if(!nodes.containsKey(diagramNameToRemove)) + throw new IllegalArgumentException("Id " + diagramNameToRemove + " not present aong the current ones"); + + nodes.remove(diagramNameToRemove); + edges.remove(diagramNameToRemove); + nodesMaps.remove(diagramNameToRemove); + edgesMaps.remove(diagramNameToRemove); + if(diagramNameNext == null){ + currentNodes = EMPTY_NODE_LIST; + currentEdges = EMPTY_EDGE_LIST; + currentNodesMap = EMPTY_NODE_MAP; + currentEdgesMap = EMPTY_EDGE_MAP; + }else{ + if(!nodes.containsKey(diagramNameNext)) + throw new IllegalArgumentException("Id " + diagramNameNext + " not present aong the current ones"); + currentNodes = nodes.get(diagramNameNext); + currentEdges = edges.get(diagramNameNext); + currentNodesMap = nodesMaps.get(diagramNameNext); + currentEdgesMap = edgesMaps.get(diagramNameNext); + } + } + + @Override + public synchronized void addNode(double x, double y, int nodeHashCode, String diagramName){ + newHapticId = true; + // waits for an identifier from the openGL thread + try{ + wait(); + }catch(InterruptedException ie){ + wasInterrupted(); + } + + Node n = new Node(x,y,nodeHashCode, currentHapticId); + if(diagramName == null){ + currentNodes.add(n); + currentNodesMap.put(currentHapticId, n); + }else{ + nodes.get(diagramName).add(n); + nodesMaps.get(diagramName).put(currentHapticId, n); + } + } + + @Override + public synchronized void removeNode(int nodeHashCode, String diagramName){ + ListIterator<Node> itr = (diagramName == null) ? currentNodes.listIterator() : nodes.get(diagramName).listIterator(); + boolean found = false; + int hID = -1; + while(itr.hasNext()){ + Node n = itr.next(); + if(n.diagramId == nodeHashCode){ + hID = n.hapticId; + itr.remove(); + found = true; + break; + } + } + assert(found); + + /* remove the node from the map as well */ + if(diagramName == null) + currentNodesMap.remove(hID); + else + nodesMaps.get(diagramName).remove(hID); + + /* set the flag to ask the haptic thread to free the id of the node that's been deleted */ + dumpHapticId = true; + /* share the id to free with the other thread */ + currentHapticId = hID; + try{ + wait(); + }catch(InterruptedException ie){ + wasInterrupted(); + } + } + + @Override + public synchronized void moveNode(double x, double y, int nodeHashCode, String diagramName){ + ArrayList<Node> iterationList = (diagramName == null) ? currentNodes : nodes.get(diagramName); + for(Node n : iterationList){ + if(n.diagramId == nodeHashCode){ + n.x = x; + n.y = y; + break; + } + } + } + + @Override + public synchronized void addEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart, int stipplePattern, Line2D attractLine, String diagramName){ + // flag the openGL thread the fact we need an identifier + newHapticId = true; + // waits for an identifier from the openGL thread + try{ + wait(); + }catch(InterruptedException ie){ + wasInterrupted(); + } + + // find the mid point of the line of attraction + double pX = Math.min(attractLine.getX1(), attractLine.getX2()); + double pY = Math.min(attractLine.getY1(), attractLine.getY2()); + pX += Math.abs(attractLine.getX1() - attractLine.getX2())/2; + pY += Math.abs(attractLine.getY1() - attractLine.getY2())/2; + + Edge e = new Edge(edgeHashCode,currentHapticId, xs, ys, adjMatrix, nodeStart, stipplePattern, pX, pY); + if(diagramName == null){ + /* add the edge reference to the Haptic edges list */ + currentEdges.add(e); + /* add the edge reference to the haptic edges map */ + currentEdgesMap.put(currentHapticId, e); + }else{ + /* add the edge reference to the Haptic edges list */ + edges.get(diagramName).add(e); + /* add the edge reference to the haptic edges map */ + edgesMaps.get(diagramName).put(currentHapticId, e); + } + } + + @Override + public synchronized void updateEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart, Line2D attractLine, String diagramName){ + assert(xs.length == ys.length); + + for(Edge e : currentEdges){ + if(e.diagramId == edgeHashCode){ + e.xs = xs; + e.ys = ys; + e.size = xs.length; + e.adjMatrix = adjMatrix; + e.nodeStart = nodeStart; + // find the mid point of the line of attraction + double pX = Math.min(attractLine.getX1(), attractLine.getX2()); + double pY = Math.min(attractLine.getY1(), attractLine.getY2()); + pX += Math.abs(attractLine.getX1() - attractLine.getX2())/2; + pY += Math.abs(attractLine.getY1() - attractLine.getY2())/2; + e.attractPointX = pX; + e.attractPointY = pY; + } + } + } + + @Override + public synchronized void removeEdge(int edgeHashCode, String diagramName){ + ListIterator<Edge> itr = (diagramName == null) ? currentEdges.listIterator() : edges.get(diagramName).listIterator(); + boolean found = false; + int hID = -1; + while(itr.hasNext()){ + Edge e = itr.next(); + if(e.diagramId == edgeHashCode){ + hID = e.hapticId; + itr.remove(); + found = true; + break; + } + } + assert(found); + + /* remove the edge from the map as well */ + if(diagramName == null) + currentEdgesMap.remove(hID); + else + edgesMaps.get(diagramName).remove(hID); + /* set the flag to ask the haptic thread to free the id of the node that's been deleted */ + dumpHapticId = true; + /* share the id to free with the other thread */ + currentHapticId = hID; + try{ + wait(); + }catch(InterruptedException ie){ + wasInterrupted(); + } + } + + @Override + public synchronized void attractTo(int elementHashCode){ + attractToHapticId = findElementHapticID(elementHashCode); + attractTo = true; + } + + @Override + public synchronized void pickUp(int elementHashCode){ + pickUpHapticId = findElementHapticID(elementHashCode); + pickUp = true; + } + + private int findElementHapticID(int elementHashCode){ + int hID = -1; + boolean found = false; + for(Node n : currentNodes){ + if(n.diagramId == elementHashCode){ + hID = n.hapticId; + found = true; + break; + } + } + + if(!found) + for(Edge e : currentEdges){ + if(e.diagramId == elementHashCode){ + hID = e.hapticId; + found = true; + break; + } + } + assert(found); + return hID; + } + + @Override + public synchronized void dispose(){ + shutdown = true; + /* wait for the haptic thread to shut down */ + try { + wait(); + } catch (InterruptedException e) { + wasInterrupted(); + } + } + + @Override + public void run() { + try { + init(width,height); + } catch (IOException e) { + throw new RuntimeException();// OMNI haptic device doesn't cause any exception + } + } + + private void wasInterrupted(){ + throw new UnsupportedOperationException("Haptics thread interrupted and no catch block implemented"); + } + + private Node getNodeFromID(int hID){ + return currentNodesMap.get(hID); + } + + private Edge getEdgeFromID(int hID){ + return currentEdgesMap.get(hID); + } + + /* the diagram currently selected */ + private ArrayList<Node> currentNodes; + private ArrayList<Edge> currentEdges; + private HashMap<Integer,Node> currentNodesMap; + private HashMap<Integer,Edge> currentEdgesMap; + + /* maps with all the diagrams in the editor */ + private HashMap<String,ArrayList<Node>> nodes; + private HashMap<String,ArrayList<Edge>> edges; + private HashMap<String,HashMap<Integer,Node>> nodesMaps; + private HashMap<String,HashMap<Integer,Edge>> edgesMaps; + private int width; + private int height; + private int attractToHapticId; + private int pickUpHapticId; + /* flag for synchronization with the haptic thread*/ + private boolean newHapticId; + private boolean dumpHapticId; + private boolean shutdown; + boolean hapticInitFailed; + private boolean attractTo; + private boolean pickUp; + /* currentHapticId is used to share haptic ids between the threads */ + private int currentHapticId; + private HapticListener hapticListener; + + private static final ArrayList<Node> EMPTY_NODE_LIST = new ArrayList<Node>(0); + private static final ArrayList<Edge> EMPTY_EDGE_LIST = new ArrayList<Edge>(0); + private static final HashMap<Integer,Node> EMPTY_NODE_MAP = new HashMap<Integer,Node>(); + private static final HashMap<Integer,Edge> EMPTY_EDGE_MAP = new HashMap<Integer,Edge>(); + +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.java Wed Apr 25 17:09:09 2012 +0100 @@ -23,12 +23,14 @@ import java.io.FilenameFilter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.text.MessageFormat; 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.SpeechOptionPane; import uk.ac.qmul.eecs.ccmi.gui.TemplateEditor; import uk.ac.qmul.eecs.ccmi.haptics.Haptics; import uk.ac.qmul.eecs.ccmi.haptics.HapticsFactory; @@ -38,6 +40,7 @@ import uk.ac.qmul.eecs.ccmi.utils.CCmIUncaughtExceptionHandler; import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; +import uk.ac.qmul.eecs.ccmi.utils.ResourceFileWriter; /** * @@ -49,8 +52,9 @@ /** * perform initialization prior to displaying the GUI + * @throws IOException */ - public void init(String[] args) { + public void init(String[] args) throws IOException { Thread.setDefaultUncaughtExceptionHandler(new CCmIUncaughtExceptionHandler()); final ResourceBundle resources = ResourceBundle.getBundle(this.getClass().getName()); /* read command line arguments */ @@ -77,47 +81,51 @@ preferences.put("home", homeDirPath); } File homeDir = new File(homeDirPath); - homeDir.mkdir(); + mkDir(homeDir, resources); File backupDir = new File(homeDir,resources.getString("dir.backups")); - backupDir.mkdir(); + mkDir(backupDir, resources); backupDirPath = backupDir.getAbsolutePath(); /* create the templates directory into the home directory */ File templateDir = new File(homeDir,resources.getString("dir.templates")); - templateDir.mkdir(); + mkDir(templateDir,resources); /* create the images directory into the home directory */ File imagesDir = new File(homeDir,resources.getString("dir.images")); - if(imagesDir.mkdir()) + if(mkDir(imagesDir,resources)) 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()) + if(mkDir(diagramDir,resources)) 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()) + if(mkDir(libsDir,resources)) preferences.put("dir.libs", libsDir.getAbsolutePath()); + /* write the template files included in the software in the template dir, if they don't exist yet */ + ResourceFileWriter resourceWriter = new ResourceFileWriter(getClass().getResource("UML Diagram.xml")); + resourceWriter.writeToDisk(templateDir.getAbsolutePath(),"UML Diagram.xml"); + resourceWriter.serResource(getClass().getResource("Tube.xml")); + resourceWriter.writeToDisk(templateDir.getAbsolutePath(),"Tube.xml"); + resourceWriter.serResource(getClass().getResource("Organization Chart.xml")); + resourceWriter.writeToDisk(templateDir.getAbsolutePath(),"Organization Chart.xml"); + /* 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]; - } + FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(File f, String name) { + return (name.endsWith(resources.getString("template.extension"))); + } + }; + templateFiles = templateDir.listFiles(filter); if(enableLog){ File logDir = new File(homeDir,resources.getString("dir.log")); - logDir.mkdir(); + mkDir(logDir,resources); try{ InteractionLog.enable(logDir.getAbsolutePath()); InteractionLog.log("PROGRAM STARTED"); @@ -128,15 +136,24 @@ } } + /* create sound, speech and haptic engines */ NarratorFactory.createInstance(); SoundFactory.createInstance(); - hapticKindle = new HapticKindle(); - HapticsFactory.createInstance(hapticKindle); + HapticsFactory.createInstance(new HapticKindle()); haptics = HapticsFactory.getInstance(); - if(!haptics.isAlive()){ - hapticKindle = null; - } + if(haptics.isAlive()) + NarratorFactory.getInstance().speakWholeText("Haptic device successfully initialized"); + } + + private boolean mkDir(File dir,ResourceBundle resources) throws IOException{ + boolean created = dir.mkdir(); + if(!dir.exists()) + throw new IOException(MessageFormat.format( + resources.getString("dir.error_msg"), + dir.getAbsolutePath()) + ); + return created; } /** @@ -145,8 +162,8 @@ @Override public void run() { editorFrame = new EditorFrame(haptics,templateFiles,backupDirPath,getTemplateEditors()); - if(hapticKindle != null) - hapticKindle.setEditorFrame(editorFrame); +// if(hapticKindle != null) +// hapticKindle.setEditorFrame(editorFrame); } public TemplateEditor[] getTemplateEditors(){ @@ -161,7 +178,24 @@ public static void main(String[] args) { DiagramEditorApp application = new DiagramEditorApp(); - application.init(args); + try { + application.init(args); + } catch (IOException e) { + final String msg = e.getLocalizedMessage(); + try { + SwingUtilities.invokeAndWait(new Runnable(){ + @Override + public void run(){ + SpeechOptionPane.showMessageDialog(null, msg); + } + }); + System.exit(-1); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } catch (InvocationTargetException ex) { + throw new RuntimeException(ex); + } + } try { SwingUtilities.invokeAndWait(application); @@ -172,8 +206,11 @@ } } - EditorFrame editorFrame; - HapticKindle hapticKindle; + public static EditorFrame getFrame(){ + return editorFrame; + } + + static EditorFrame editorFrame; Haptics haptics; File[] templateFiles; TemplateEditor[] templateCreators;
--- a/java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.properties Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.properties Wed Apr 25 17:09:09 2012 +0100 @@ -9,6 +9,9 @@ dir.log=log usage=Unrecognized option(s)\nUsage : java -jar ccmi.jar [-l] \n -l : enables interaction log +dir.error_msg=Could not create the following directory: {0}\u000A\ +Please check directory permissions and try again. + #### APPLICATION PREFERENCES #### # server.local_port @@ -20,3 +23,8 @@ # laf # recent # use_accessible_filechooser +# user.id +# server.address +# server.local_port +# server.remote_port +# second_voice_enabled
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/Organization Chart.xml Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<Diagram Name="Organization Chart" PrototypeDelegate="uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapePrototypePersistenceDelegate"> + <Prototypes> + <Node> + <Type>Entity</Type> + <ShapeType>Rectangle</ShapeType> + <Properties> + <Property> + <Type>Responsibility</Type> + <View Position="Inside" ShapeType="Rectangle"/> + </Property> + </Properties> + </Node> + <Edge> + <Type>Connection</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + </Prototypes> +</Diagram>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/Tube.xml Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<Diagram Name="Tube" PrototypeDelegate="uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapePrototypePersistenceDelegate"> + <Prototypes> + <Node> + <Type>Stations</Type> + <ShapeType>Circle</ShapeType> + <Properties/> + </Node> + <Edge> + <Type>Central Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + <Edge> + <Type>Victoria Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + <Edge> + <Type>Jubilee Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + <Edge> + <Type>Piccadilly Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + <Edge> + <Type>Circle Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + </Prototypes> +</Diagram>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/UML Diagram.xml Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<Diagram Name="UML Diagram" PrototypeDelegate="uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapePrototypePersistenceDelegate"> + <Prototypes> + <Node> + <Type>Class</Type> + <ShapeType>Rectangle</ShapeType> + <Properties> + <Property> + <Type>attributes</Type> + <View Position="Inside" ShapeType="Transparent"/> + <Modifiers> + <Modifier id="0"> + <Type>static</Type> + <View Bold="false" Italic="true" Prefix="" Suffix="" Underline="false"/> + </Modifier> + <Modifier id="1"> + <Type>protected</Type> + <View Bold="false" Italic="false" Prefix="#" Suffix="" Underline="false"/> + </Modifier> + <Modifier id="2"> + <Type>private</Type> + <View Bold="false" Italic="false" Prefix="-" Suffix="" Underline="false"/> + </Modifier> + </Modifiers> + </Property> + <Property> + <Type>operations</Type> + <View Position="Inside" ShapeType="Transparent"/> + <Modifiers> + <Modifier id="0"> + <Type>static</Type> + <View Bold="false" Italic="true" Prefix="" Suffix="" Underline="false"/> + </Modifier> + <Modifier id="1"> + <Type>protected</Type> + <View Bold="false" Italic="false" Prefix="#" Suffix="" Underline="false"/> + </Modifier> + <Modifier id="2"> + <Type>private</Type> + <View Bold="false" Italic="false" Prefix="-" Suffix="" Underline="false"/> + </Modifier> + </Modifiers> + </Property> + </Properties> + </Node> + <Edge> + <Type>Association</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + <Heads> + <Head Head="Tail" headLabel="from"/> + <Head Head="V" headLabel="to"/> + </Heads> + </Edge> + <Edge> + <Type>Generalization</Type> + <LineStyle>Dashed</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + <Heads> + <Head Head="Tail" headLabel="sub class"/> + <Head Head="Triangle" headLabel="super class"/> + </Heads> + </Edge> + </Prototypes> +</Diagram>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/AwarenessMessage.java Wed Apr 25 17:09:09 2012 +0100 @@ -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.network; + +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +public class AwarenessMessage extends Message { + + public AwarenessMessage(long timestamp, Name name, String diagram, DiagramEventActionSource source) { + super(timestamp, diagram, source); + this.name = name; + } + + public AwarenessMessage(Name name, String diagram, DiagramEventActionSource source) { + super(diagram, source); + this.name = name; + } + + public AwarenessMessage(Name name, String diagram, String source) { + super(diagram, source); + this.name = name; + } + + @Override + public Name getName() { + return name; + } + + public static Name valueOf(String n){ + Name name = Name.NONE_A; + try { + name = Name.valueOf(n); + }catch(IllegalArgumentException iae){ + iae.printStackTrace(); + } + return name; + } + + public final static String NAME_POSTFIX = "_A"; + public static final String USERNAMES_SEPARATOR = "\n"; + private Name name; + private static String defaultUserName = PreferencesService.getInstance().get("user.name", System.getProperty("user.name")); + + public enum Name implements Message.MessageName { + START_A, + STOP_A, + GOON_A, + USERNAME_A, + ERROR_A, + NONE_A; + }; + + public static String getDefaultUserName(){ + synchronized(AwarenessMessage.class){ + return defaultUserName; + } + } + + public static void setDefaultUserName(String userName){ + synchronized(AwarenessMessage.class){ + defaultUserName = userName ; + } + } +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/CCmIOSCPacketCodec.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/CCmIOSCPacketCodec.java Wed Apr 25 17:09:09 2012 +0100 @@ -51,6 +51,5 @@ super.encodeBundle(bndl, b); b.position("#bundle\0".length()); long timestamp = b.getLong(); - System.out.println("time in encode b" + timestamp); }*/ }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/ClientConnectionManager.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ClientConnectionManager.java Wed Apr 25 17:09:09 2012 +0100 @@ -26,12 +26,10 @@ 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; @@ -41,18 +39,19 @@ 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.diagrammodel.DiagramTreeNode; import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; 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.gui.awareness.DisplayFilter; +import uk.ac.qmul.eecs.ccmi.speech.Narrator; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; /** @@ -63,10 +62,10 @@ * issued by the server, the element is created from scratch, according to the message of the server. * */ -public class ClientConnectionManager extends Thread { +public class ClientConnectionManager extends NetworkThread { public ClientConnectionManager(EditorTabbedPane tabbedPane) throws IOException{ - super("Client Connection Manager"); + super("Network Client Thread"); channels = new HashMap<SocketChannel, Diagram>(); requests = new ConcurrentLinkedQueue<Request>(); answers = new LinkedBlockingQueue<Answer>(); @@ -75,7 +74,7 @@ this.tabbedPane = tabbedPane; protocol = ProtocolFactory.newInstance(); mustSayGoodbye = false; - mustAnswer = false; + waitingAnswer = false; } /** @@ -85,12 +84,19 @@ */ public void addRequest(Request r){ requests.add(r); + if(r instanceof SendLockRequest){ + SendLockRequest slr = (SendLockRequest) r; + if(slr.lock.getName().toString().startsWith(LockMessage.GET_LOCK_PREFIX)) + waitingAnswer = true; + } selector.wakeup(); } public Answer getAnswer(){ try { - return answers.take(); + Answer answer = answers.take(); + waitingAnswer = false; + return answer; } catch (InterruptedException e) { throw new RuntimeException(e);// must never happen } @@ -108,7 +114,7 @@ if(mustSayGoodbye) break; - /* handle the requests for the server from the local users */ + /* handle the requests for the remote server from the local users */ handleRequests(); for (Iterator<SelectionKey> itr = selector.selectedKeys().iterator(); itr.hasNext();){ @@ -124,111 +130,106 @@ try { msg = protocol.receiveMessage(channel); } catch (IOException e) { - revertDiagram(channel); + revertDiagram(channel,false); /* signal the event dispatching thread, otherwise blocked */ - if(mustAnswer){ - try { - answers.put(new ErrorAnswer()); - } catch (InterruptedException ie) { - throw new RuntimeException(ie); - } + try { + /* RevertedDiagramAnswer is to prevent the Event dispatching Thread from blocking if the * + * server goes down and the client is still waiting for an answer. If the thread * + * was not waiting for an answers the RevertedDiagramAnswer will not be put in the queue */ + if(waitingAnswer) + answers.put(new RevertedDiagramAnswer()); + } 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; + cmd.getSource().setDiagramName(diagram.getName()); /* 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)); + 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(), DiagramEventSource.NONE); + /* perform the actual translation from the origin */ + node.translate(new Point2D.Double(),dx,dy,DiagramEventSource.NONE); + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), node, null, cmd.getSource())); 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)); - } + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((String)cmd.getArgAt(0), diagram.getEdgePrototypes()); + edge = (Edge)edge.clone(); + long[] nodesToConnect = new long[cmd.getArgNum()-1]; + for(int i=0;i<nodesToConnect.length;i++) + nodesToConnect[i] = (Long)cmd.getArgAt(i+1); + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), edge, nodesToConnect, cmd.getSource())); + diagram.getCollectionModel().getMonitor().unlock(); 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)); + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(),node,cmd.getSource())); 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)); + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0), diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(),edge,cmd.getSource())); 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))); + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0), diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetName(edge, (String)cmd.getArgAt(1),cmd.getSource())); 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))); + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetName(node, (String)cmd.getArgAt(1),cmd.getSource())); break; case SET_PROPERTY : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.SetProperty( node, (String)cmd.getArgAt(1), (Integer)cmd.getArgAt(2), - (String)cmd.getArgAt(3) + (String)cmd.getArgAt(3), + cmd.getSource() )); 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)); + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.SetProperties( node, - properties + (String)cmd.getArgAt(1), + cmd.getSource() )); break; case CLEAR_PROPERTIES : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } - SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node)); + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node,cmd.getSource())); break; case SET_NOTES : int[] path = new int[cmd.getArgNum()-1]; @@ -236,89 +237,92 @@ 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)); + diagram.getCollectionModel().getMonitor().lock(); + treeNode = Finder.findTreeNode(path, (DiagramTreeNode)diagram.getTreeModel().getRoot()); + diagram.getCollectionModel().getMonitor().unlock();; + SwingUtilities.invokeLater(new CommandExecutor.SetNotes(diagram.getTreeModel(),treeNode, notes,cmd.getSource())); break; case ADD_PROPERTY : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.AddProperty( node, (String)cmd.getArgAt(1), - (String)cmd.getArgAt(2) + (String)cmd.getArgAt(2), + cmd.getSource() )); break; case REMOVE_PROPERTY : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( node, (String)cmd.getArgAt(1), - (Integer)cmd.getArgAt(2))); + (Integer)cmd.getArgAt(2), + cmd.getSource())); 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)); - } + diagram.getCollectionModel().getMonitor().lock(); + 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)); } + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.SetModifiers( node, (String)cmd.getArgAt(1), (Integer)cmd.getArgAt(2), - indexes - )); + indexes, + cmd.getSource())); 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))); + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel(edge, node,(String)cmd.getArgAt(2),cmd.getSource())); 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()); - } + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription( edge, node, - (Integer)cmd.getArgAt(2) + (Integer)cmd.getArgAt(2), + cmd.getSource() )); break; case TRANSLATE_NODE : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); 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) - )); + (Double)cmd.getArgAt(4), + cmd.getSource())); break; case TRANSLATE_EDGE : - synchronized(diagram.getCollectionModel().getMonitor()){ - edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); - } + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); 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) - )); + (Double)cmd.getArgAt(4), + cmd.getSource())); break; case BEND : - synchronized(diagram.getCollectionModel().getMonitor()){ - edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); - } + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); Point2D bendStart = null; if(cmd.getArgNum() == 5){ bendStart = new Point2D.Double((Double)cmd.getArgAt(3),(Double)cmd.getArgAt(4)); @@ -326,18 +330,18 @@ SwingUtilities.invokeLater(new CommandExecutor.Bend( edge, new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), - bendStart - )); + bendStart, + cmd.getSource())); 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)); + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge,cmd.getSource())); break; case STOP_NODE_MOVE : node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); - SwingUtilities.invokeLater(new CommandExecutor.StopMove(node)); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(node,cmd.getSource())); break; } }else if(msg instanceof Reply){ @@ -356,56 +360,55 @@ switch(reply.getName()){ case INSERT_NODE_R : case INSERT_EDGE_R : - SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), sendCmdRequest.element)); + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), sendCmdRequest.element, null, reply.getSource())); break; case REMOVE_EDGE_R : case REMOVE_NODE_R : - SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(), sendCmdRequest.element)); + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(), sendCmdRequest.element,reply.getSource())); break; case SET_EDGE_NAME_R : case SET_NODE_NAME_R : - SwingUtilities.invokeLater(new CommandExecutor.SetName(sendCmdRequest.element, (String)sendCmdRequest.cmd.getArgAt(1))); + SwingUtilities.invokeLater(new CommandExecutor.SetName(sendCmdRequest.element, (String)sendCmdRequest.cmd.getArgAt(1),reply.getSource())); 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) + (String)sendCmdRequest.cmd.getArgAt(3), + reply.getSource() )); 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 - )); + (String)sendCmdRequest.cmd.getArgAt(1), + reply.getSource())); break; case CLEAR_PROPERTIES_R : - SwingUtilities.invokeLater(new CommandExecutor.ClearProperties((Node)sendCmdRequest.element)); + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties((Node)sendCmdRequest.element,reply.getSource())); break; case SET_NOTES_R : SwingUtilities.invokeLater(new CommandExecutor.SetNotes( diagram.getTreeModel(), ((SendTreeCmdRequest)sendCmdRequest).treeNode, - (String)sendCmdRequest.cmd.getArgAt(sendCmdRequest.cmd.getArgNum()-1) - )); + (String)sendCmdRequest.cmd.getArgAt(sendCmdRequest.cmd.getArgNum()-1), + reply.getSource())); break; case ADD_PROPERTY_R : SwingUtilities.invokeLater(new CommandExecutor.AddProperty( (Node)sendCmdRequest.element, (String)sendCmdRequest.cmd.getArgAt(1), - (String)sendCmdRequest.cmd.getArgAt(2) - )); + (String)sendCmdRequest.cmd.getArgAt(2), + reply.getSource())); break; case REMOVE_PROPERTY_R : SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( (Node)sendCmdRequest.element, (String)sendCmdRequest.cmd.getArgAt(1), - (Integer)sendCmdRequest.cmd.getArgAt(2) - )); + (Integer)sendCmdRequest.cmd.getArgAt(2), + reply.getSource())); break; case SET_MODIFIERS_R : indexes = new HashSet<Integer>(sendCmdRequest.cmd.getArgNum()-3); @@ -416,8 +419,8 @@ (Node)sendCmdRequest.element, (String)sendCmdRequest.cmd.getArgAt(1), (Integer)sendCmdRequest.cmd.getArgAt(2), - indexes - )); + indexes, + reply.getSource())); break; case SET_ENDLABEL_R : synchronized(diagram.getCollectionModel().getMonitor()){ @@ -426,8 +429,8 @@ SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel( (Edge)sendCmdRequest.element, node, - (String)sendCmdRequest.cmd.getArgAt(2) - )); + (String)sendCmdRequest.cmd.getArgAt(2), + reply.getSource())); break; case SET_ENDDESCRIPTION_R : synchronized(diagram.getCollectionModel().getMonitor()){ @@ -437,8 +440,8 @@ (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 - )); + sendCmdRequest.cmd.getArgNum() == 3 ? (Integer)sendCmdRequest.cmd.getArgAt(2): -1, + reply.getSource())); break; case TRANSLATE_NODE_R : node = (Node)sendCmdRequest.element; @@ -446,8 +449,8 @@ node, new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), (Double)sendCmdRequest.cmd.getArgAt(3), - (Double)sendCmdRequest.cmd.getArgAt(4) - )); + (Double)sendCmdRequest.cmd.getArgAt(4), + reply.getSource())); break; case TRANSLATE_EDGE_R : edge = (Edge)sendCmdRequest.element; @@ -455,7 +458,8 @@ edge, new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), (Double)sendCmdRequest.cmd.getArgAt(3), - (Double)sendCmdRequest.cmd.getArgAt(4) + (Double)sendCmdRequest.cmd.getArgAt(4), + reply.getSource() )); break; case BEND_R : @@ -467,30 +471,72 @@ SwingUtilities.invokeLater(new CommandExecutor.Bend( edge, new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), - bendStart + bendStart, + reply.getSource() )); break; case STOP_EDGE_MOVE_R : edge = (Edge)sendCmdRequest.element; - SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge)); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge,reply.getSource())); break; case STOP_NODE_MOVE_R : node = (Node)sendCmdRequest.element; - SwingUtilities.invokeLater(new CommandExecutor.StopMove(node)); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(node,reply.getSource())); break; case ERROR_R : - SwingUtilities.invokeLater(new CommandExecutor.ShowErrorMessageDialog(tabbedPane, "Error for command on "+ sendCmdRequest.element.getName()+ ". " +reply.getMessage())); + SwingUtilities.invokeLater(new CommandExecutor.ShowErrorMessageDialog(tabbedPane, "Error for command on "+ sendCmdRequest.element.getName()+ ". " +reply.getMessage(),reply.getSource())); 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 + }else if(msg instanceof LockMessage){ // lock message from the server try { answers.put(new LockAnswer((LockMessage)msg)); - mustAnswer = false; } catch (InterruptedException e) { throw new RuntimeException(e); // must never happen } + }else{ // awareness message + AwarenessMessage awMsg = (AwarenessMessage)msg; + DisplayFilter filter = DisplayFilter.getInstance(); + if(filter != null){// if awareness panel ain't open, drop the packet + switch(awMsg.getName()){ + case START_A :{ + if(filter.configurationHasChanged()){ + for(Diagram d : channels.values()) + getAwarenessPanelEditor().clearRecords(d.getName()); + } + DiagramEventActionSource actionSource = (DiagramEventActionSource)awMsg.getSource(); + if(actionSource.getCmd() == Command.Name.INSERT_NODE || actionSource.getCmd() == Command.Name.INSERT_EDGE || actionSource.getCmd() == Command.Name.SELECT_NODE_FOR_EDGE_CREATION) + getAwarenessPanelEditor().addTimedRecord(diagramName, filter.processForText(actionSource)); + else + getAwarenessPanelEditor().addRecord(diagramName, filter.processForText(actionSource)); + /* announce the just received awareness message via the second voice */ + NarratorFactory.getInstance().speakWholeText(filter.processForSpeech(actionSource), Narrator.SECOND_VOICE); + }break; + case STOP_A : { + if(filter.configurationHasChanged()){ + for(Diagram d : channels.values()) + getAwarenessPanelEditor().clearRecords(d.getName()); + } + DiagramEventActionSource actionSource = (DiagramEventActionSource)awMsg.getSource(); + /* unselect node for edge creation is announced and put temporarily on the text panel * + * (select node for edge creation is temporary as well so we don't need to clean the panel) */ + if(actionSource.getCmd() == Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION){ + getAwarenessPanelEditor().addTimedRecord(diagramName, filter.processForText(actionSource)); + NarratorFactory.getInstance().speakWholeText(filter.processForSpeech(actionSource)); + }else{ + getAwarenessPanelEditor().removeRecord(diagramName,filter.processForText(actionSource)); + } + }break; + case USERNAME_A : { + String userNames = (String)awMsg.getSource(); + getAwarenessPanelEditor().replaceUserName(diagramName,userNames); + }break; + case ERROR_A : { + SpeechOptionPane.showMessageDialog(tabbedPane, (String)awMsg.getSource()); + }break; + } + } } } } @@ -519,17 +565,19 @@ /* 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); + revertDiagram(adr.channel,false); } - }else if(request instanceof RmDiagramRequest){ + }else if(request instanceof RmDiagramRequest){ // user un-shared a diagram RmDiagramRequest rdr = (RmDiagramRequest)request; Set<Map.Entry<SocketChannel, Diagram>> entryset = channels.entrySet(); + SocketChannel channel = null; 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();} + channel = entry.getKey(); } } + if(channel != null) + revertDiagram(channel,true); }else if(request instanceof SendCmdRequest||request instanceof SendTreeCmdRequest){ SendCmdRequest scr = (SendCmdRequest)request; //System.out.println("ClientConnectionManager:handling request "+scr.cmd.getName()); @@ -541,28 +589,36 @@ }catch(IOException e){ /* the pending commands is normally removed upon reply receive */ pendingCommands.remove(scr); - revertDiagram(scr.channel); + revertDiagram(scr.channel,false); } }else if(request instanceof SendLockRequest){ SendLockRequest slr = (SendLockRequest)request; try { protocol.send(slr.channel,slr.lock); - mustAnswer = true; } catch (IOException e) { - revertDiagram(slr.channel); + revertDiagram(slr.channel,false); 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 + /* RevertedDiagramAnswer is to prevent the Event dispatching Thread from blocking if the * + * server goes down and the client is still waiting for an answer. If the thread * + * was not waiting for an answers the RevertedDiagramAnswer will not be put in the queue */ + if(waitingAnswer) + answers.put(new RevertedDiagramAnswer()); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); //must never happen } } + }else if(request instanceof SendAwarenessRequest){ + SendAwarenessRequest awr = (SendAwarenessRequest)request; + try{ + protocol.send(awr.channel, awr.awMsg); + }catch (IOException e) { + revertDiagram(awr.channel,false); + } } } } - private void revertDiagram(SocketChannel c){ + private void revertDiagram(SocketChannel c,final boolean userRequest){ /* from now on all the commands using this channel will be dropped */ final Diagram diagram = channels.remove(c); if(diagram == null) @@ -577,15 +633,17 @@ NetDiagram netDiagram = (NetDiagram)dPanel.getDiagram(); if( netDiagram.getDelegate().equals(diagram)){ /* set the old (unwrapped) diagram as the current one */ + dPanel.setAwarenessPanelEnabled(false); dPanel.setDiagram(diagram); break; } } } - SpeechOptionPane.showMessageDialog(tabbedPane, MessageFormat.format( - ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.connection"), - diagram.getName()) - ); + if(!userRequest)// show the message only if the revert is due to an error + SpeechOptionPane.showMessageDialog(tabbedPane, MessageFormat.format( + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.connection"), + diagram.getName()) + ); } }); @@ -603,8 +661,9 @@ 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()); + /* set the old (unwrapped) diagram as the current one */ + dPanel.setAwarenessPanelEnabled(false); + dPanel.setDiagram(netDiagram.getDelegate()); } } SpeechOptionPane.showMessageDialog( @@ -647,11 +706,11 @@ } public static class SendTreeCmdRequest extends SendCmdRequest{ - public SendTreeCmdRequest( Command cmd,SocketChannel channel,DiagramModelTreeNode treeNode) { + public SendTreeCmdRequest( Command cmd,SocketChannel channel,DiagramTreeNode treeNode) { super(cmd,channel,null); this.treeNode = treeNode; } - public DiagramModelTreeNode treeNode; + public DiagramTreeNode treeNode; public SocketChannel channel; public Command cmd; } @@ -665,6 +724,15 @@ public LockMessage lock; } + public static class SendAwarenessRequest implements Request { + public SendAwarenessRequest(SocketChannel channel, AwarenessMessage awMsg){ + this.awMsg = awMsg; + this.channel = channel; + } + public SocketChannel channel; + public AwarenessMessage awMsg; + } + public interface Answer {}; public static class LockAnswer implements Answer { public LockAnswer(LockMessage answer){ @@ -673,13 +741,11 @@ public LockMessage message; } - public static class ErrorAnswer implements Answer{ - - } + public static class RevertedDiagramAnswer implements Answer{} private Node node; private Edge edge; - private DiagramModelTreeNode treeNode; + private DiagramTreeNode treeNode; private Set<Integer> indexes; private SendCmdRequest sendCmdRequest; /* for each server hold the diagram it shares with it */ @@ -690,6 +756,6 @@ private Selector selector; private EditorTabbedPane tabbedPane; private Protocol protocol; - private boolean mustAnswer; + private volatile boolean waitingAnswer; private volatile boolean mustSayGoodbye; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/Command.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Command.java Wed Apr 25 17:09:09 2012 +0100 @@ -19,6 +19,7 @@ package uk.ac.qmul.eecs.ccmi.network; +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; import uk.ac.qmul.eecs.ccmi.utils.CharEscaper; import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; @@ -31,26 +32,32 @@ * */ public class Command extends Message { - public Command(Name name, String diagram, Object[] args, long timestamp){ - super(timestamp,diagram); + public Command(Name name, String diagram, Object[] args, long timestamp, DiagramEventSource source){ + super(timestamp,diagram,source); this.name = name; this.args = args; } - public Command(Name name, String diagram, Object[] args){ - super(diagram); + public Command(Name name, String diagram, Object[] args, DiagramEventSource source){ + super(diagram,source); this.name = name; this.args = args; } - public Command(Name name, String diagram, long timestamp){ - this(name, diagram); + public Command(Name name, String diagram, long timestamp, DiagramEventSource source){ + this(name, diagram, source); } - public Command(Name name, String diagram){ - this(name, diagram, new Object[]{}); + public Command(Name name, String diagram, DiagramEventSource source){ + this(name, diagram, new Object[]{},source); } + @Override + public DiagramEventSource getSource(){ + return (DiagramEventSource)super.getSource(); + } + + @Override public Name getName() { return name; } @@ -68,6 +75,11 @@ } + /** + * Utility method to log, through the interaction log, the receipt of a command + * @param cmd the received command + * @param action + */ 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){ @@ -90,7 +102,7 @@ try { name = Name.valueOf(n); }catch(IllegalArgumentException iae){ - name.setOrigin(n); + iae.printStackTrace(); } return name; } @@ -122,23 +134,14 @@ 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; + STOP_NODE_MOVE, + /** + * not a proper command, only used for awareness on node selection for edge creation. + */ + SELECT_NODE_FOR_EDGE_CREATION, + /** + * not a proper command, only used for awareness on node un-selection for edge creation. + */ + UNSELECT_NODE_FOR_EDGE_CREATION; } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/CommandExecutor.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/CommandExecutor.java Wed Apr 25 17:09:09 2012 +0100 @@ -21,14 +21,20 @@ import java.awt.Component; import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; import java.util.Set; 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.DiagramTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; import uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel; +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Finder; import uk.ac.qmul.eecs.ccmi.gui.GraphElement; import uk.ac.qmul.eecs.ccmi.gui.Node; import uk.ac.qmul.eecs.ccmi.gui.SpeechOptionPane; @@ -40,44 +46,74 @@ * */ public abstract class CommandExecutor implements Runnable { + protected CommandExecutor(DiagramEventSource source){ + this.source = source; + } + protected DiagramEventSource source; public static class Insert extends CommandExecutor{ - public Insert(CollectionModel<Node,Edge> m, DiagramElement de){ + public Insert(CollectionModel<Node,Edge> m, DiagramElement de, long[] nds, DiagramEventSource source){ + super(source); element = de; model = m; + nodes = nds; } @Override public void run() { - if(element instanceof Node) - model.insert((Node)element); - else - model.insert((Edge)element); + model.getMonitor().lock(); + if(element instanceof Node){ + model.insert((Node)element,source); + }else{ + Edge edge = (Edge)element; + if(nodes != null){ + List<DiagramNode> nodesToConnect = new ArrayList<DiagramNode>(nodes.length); + /* retrieve the nodes to connect by the id, conveyed in the message */ + for(int i = 0; i<nodes.length; i++){ + Node attachedNode = Finder.findNode(nodes[i],model.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 + } + } + model.insert(edge,source); + } + model.getMonitor().unlock(); } private CollectionModel<Node,Edge> model; private DiagramElement element; + private long[] nodes; } public static class Remove extends CommandExecutor{ - public Remove(CollectionModel<Node,Edge> m, DiagramElement de) { + public Remove(CollectionModel<Node,Edge> m, DiagramElement de, DiagramEventSource source) { + super(source); model = m; element = de; } @Override public void run() { - model.takeOut(element); + model.getMonitor().lock(); + model.takeOut(element,source); + model.getMonitor().unlock(); } private CollectionModel<Node,Edge> model; private DiagramElement element; } public static class SetName extends CommandExecutor { - public SetName(DiagramElement de, String n){ + public SetName(DiagramElement de, String n, DiagramEventSource source){ + super(source); element = de; name = n; } @Override public void run(){ - element.setName(name); + element.getMonitor().lock(); + element.setName(name,source); + element.getMonitor().unlock(); } private String name; private DiagramElement element; @@ -85,7 +121,8 @@ public static class SetProperty extends CommandExecutor{ - public SetProperty(Node n, String t, Integer i, String v){ + public SetProperty(Node n, String t, Integer i, String v, DiagramEventSource source){ + super(source); node = n; type = t; index = i; @@ -94,7 +131,9 @@ @Override public void run(){ - node.setProperty(type, index, value); + node.getMonitor().lock(); + node.setProperty(type, index, value,source); + node.getMonitor().unlock(); } private Node node; @@ -104,31 +143,40 @@ } public static class SetProperties extends CommandExecutor { - public SetProperties(Node n, NodeProperties p){ + public SetProperties(Node n, String p, DiagramEventSource source){ + super(source); node = n; - properties = p; + propertiesAsString = p; } @Override public void run(){ - node.setProperties(properties); + node.getMonitor().lock(); + NodeProperties properties = node.getProperties(); + properties.fill(propertiesAsString); + node.setProperties(properties,source); + node.getMonitor().unlock(); } private Node node; - private NodeProperties properties; + private String propertiesAsString; } public static class ClearProperties extends CommandExecutor { - public ClearProperties(Node n){ + public ClearProperties(Node n, DiagramEventSource source){ + super(source); node = n; } @Override public void run(){ - node.clearProperties(); + node.getMonitor().lock(); + node.clearProperties(source); + node.getMonitor().unlock(); } private Node node; } public static class SetNotes extends CommandExecutor{ - public SetNotes(TreeModel<Node,Edge> m, DiagramModelTreeNode tn, String n){ + public SetNotes(TreeModel<Node,Edge> m, DiagramTreeNode tn, String n, DiagramEventSource source){ + super(source); model = m; treeNode = tn; notes = n; @@ -136,22 +184,27 @@ @Override public void run(){ - model.setNotes(treeNode, notes); + model.getMonitor().lock(); + model.setNotes(treeNode, notes,source); + model.getMonitor().unlock(); } - private DiagramModelTreeNode treeNode; + private DiagramTreeNode treeNode; private String notes; private TreeModel<Node,Edge> model; } public static class AddProperty extends CommandExecutor { - public AddProperty(Node n, String t, String v){ + public AddProperty(Node n, String t, String v, DiagramEventSource source){ + super(source); node = n; type = t; value = v; } @Override public void run(){ - node.addProperty(type, value); + node.getMonitor().lock(); + node.addProperty(type, value,source); + node.getMonitor().unlock(); } private Node node; private String type; @@ -159,7 +212,8 @@ } public static class RemoveProperty extends CommandExecutor { - public RemoveProperty(Node n, String t, int i){ + public RemoveProperty(Node n, String t, int i, DiagramEventSource source){ + super(source); node = n; type = t; index = i; @@ -167,7 +221,9 @@ @Override public void run(){ - node.removeProperty(type, index); + node.getMonitor().lock(); + node.removeProperty(type, index,source); + node.getMonitor().unlock(); } private Node node; @@ -176,7 +232,8 @@ } public static class SetModifiers extends CommandExecutor { - public SetModifiers(Node n,String t,Integer v, Set<Integer> i){ + public SetModifiers(Node n,String t,Integer v, Set<Integer> i, DiagramEventSource source){ + super(source); node = n; type = t; value = v; @@ -184,7 +241,9 @@ } @Override public void run(){ - node.setModifierIndexes(type, value, indexes); + node.getMonitor().lock(); + node.setModifierIndexes(type, value, indexes,source); + node.getMonitor().unlock(); } private Node node; private String type; @@ -193,14 +252,17 @@ } public static class SetEndLabel extends CommandExecutor { - public SetEndLabel(Edge e, Node n, String l){ + public SetEndLabel(Edge e, Node n, String l, DiagramEventSource source){ + super(source); edge = e; node = n; label = l; } @Override public void run(){ - edge.setEndLabel(node, label); + edge.getMonitor().lock(); + edge.setEndLabel(node, label,source); + edge.getMonitor().unlock(); } private Node node; private Edge edge; @@ -208,14 +270,17 @@ } public static class SetEndDescription extends CommandExecutor { - public SetEndDescription(Edge e, Node n, int i){ + public SetEndDescription(Edge e, Node n, int i, DiagramEventSource source){ + super(source); edge = e; node = n; index = i; } @Override public void run(){ - edge.setEndDescription(node, index); + edge.getMonitor().lock(); + edge.setEndDescription(node, index,source); + edge.getMonitor().unlock(); } private Node node; private Edge edge; @@ -223,7 +288,8 @@ } public static class Translate extends CommandExecutor { - public Translate(GraphElement ge, Point2D p, Double x, Double y){ + public Translate(GraphElement ge, Point2D p, Double x, Double y, DiagramEventSource source){ + super(source); element = ge; point = p; dx = x; @@ -232,7 +298,15 @@ @Override public void run(){ - element.translate(point, dx, dy); + if(element instanceof Node) + ((Node)element).getMonitor().lock(); + else + ((Edge)element).getMonitor().lock(); + element.translate(point, dx, dy,source); + if(element instanceof Node) + ((Node)element).getMonitor().unlock(); + else + ((Edge)element).getMonitor().unlock(); } private GraphElement element; @@ -242,29 +316,41 @@ } public static class StartMove extends CommandExecutor { - public StartMove(GraphElement ge, Point2D p){ + public StartMove(GraphElement ge, Point2D p, DiagramEventSource source){ + super(source); element = ge; point = p; } @Override public void run(){ - element.startMove(point); + if(element instanceof Node) + ((Node)element).getMonitor().lock(); + else + ((Edge)element).getMonitor().lock(); + element.startMove(point,source); + if(element instanceof Node) + ((Node)element).getMonitor().unlock(); + else + ((Edge)element).getMonitor().unlock(); } private GraphElement element; private Point2D point; } public static class Bend extends CommandExecutor { - public Bend(Edge e, Point2D p, Point2D bs){ + public Bend(Edge e, Point2D p, Point2D bs, DiagramEventSource source){ + super(source); edge = e; point = p; bendStart = bs; } @Override public void run(){ + edge.getMonitor().lock(); if(bendStart != null) - edge.startMove(bendStart); - edge.bend(point); + edge.startMove(bendStart,source); + edge.bend(point,source); + edge.getMonitor().unlock(); } private Edge edge; private Point2D point; @@ -272,18 +358,28 @@ } public static class StopMove extends CommandExecutor { - public StopMove(GraphElement ge){ + public StopMove(GraphElement ge, DiagramEventSource source){ + super(source); element = ge; } @Override public void run(){ - element.stopMove(); + if(element instanceof Node) + ((Node)element).getMonitor().lock(); + else + ((Edge)element).getMonitor().lock(); + element.stopMove(source); + if(element instanceof Node) + ((Node)element).getMonitor().unlock(); + else + ((Edge)element).getMonitor().unlock(); } private GraphElement element; } public static class ShowErrorMessageDialog extends CommandExecutor { - public ShowErrorMessageDialog(Component c, String msg){ + public ShowErrorMessageDialog(Component c, String msg, DiagramEventSource source){ + super(source); message = msg; parentComponent = c; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramDownloader.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramDownloader.java Wed Apr 25 17:09:09 2012 +0100 @@ -23,6 +23,7 @@ import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; import uk.ac.qmul.eecs.ccmi.gui.SpeechOptionPane; import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; @@ -37,10 +38,10 @@ */ public class DiagramDownloader extends SpeechOptionPane.ProgressDialogWorker<String,Void> { - public DiagramDownloader(SocketChannel channel, String target, Task task){ + public DiagramDownloader(SocketChannel channel, String target, int task){ this.channel = channel; this.task = task; - if(task == Task.CONNECT_AND_DOWNLOAD_LIST) + if(task == CONNECT_AND_DOWNLOAD_LIST_TASK) this.address = target; else this.diagramName = target; @@ -48,18 +49,18 @@ @Override protected String doInBackground() throws Exception { - if(task == Task.CONNECT_AND_DOWNLOAD_LIST){ + if(task == CONNECT_AND_DOWNLOAD_LIST_TASK){ 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,"")); + case CONNECT_AND_DOWNLOAD_LIST_TASK : + protocol.send(channel, new Command(Command.Name.LIST,"",DiagramEventSource.NONE)); break; - case DOWNLOAD_DIAGRAM : - protocol.send(channel, new Command(Command.Name.GET ,diagramName)); + case DOWNLOAD_DIAGRAM_TASK : + protocol.send(channel, new Command(Command.Name.GET ,diagramName,DiagramEventSource.NONE)); } Reply reply = protocol.receiveReply(channel); switch(reply.getName()){ @@ -76,13 +77,11 @@ } } - public static enum Task{ - CONNECT_AND_DOWNLOAD_LIST, - DOWNLOAD_DIAGRAM; - }; + public static final int CONNECT_AND_DOWNLOAD_LIST_TASK = 0; + public static final int DOWNLOAD_DIAGRAM_TASK = 1; private SocketChannel channel; private String diagramName; private String address; - private Task task; + private int task; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramEventActionSource.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,151 @@ +/* + 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.DiagramEventSource; + +/** + * This class represent a source of an editing action. An editing action is initiated when + * the user gets the lock on a certain element and terminates when the user after changing + * the diagram model somehow yields the lock back to the server. + * + */ +public class DiagramEventActionSource extends DiagramEventSource{ + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public DiagramEventActionSource (DiagramEventSource eventSource, Command.Name cmd, long elementID, String elementName) { + super(eventSource); + this.cmd = cmd; + this.elementID = elementID; + this.saveID = elementID; + this.elementName = elementName; + userName = AwarenessMessage.getDefaultUserName(); + timestamp = System.currentTimeMillis(); + } + + public Command.Name getCmd() { + return cmd; + } + + public long getElementID(){ + return elementID; + } + + public void setElementID(long ID){ + elementID = ID; + } + + long getSaveID(){ + return saveID; + } + + public String getElementName(){ + return elementName; + } + + public void setElementName(String elementName){ + this.elementName = elementName; + } + + public void setUserName(String name){ + this.userName = name; + } + + public String getUserName(){ + return userName; + } + + /** + * + * The local user never gets this informations from itself are they are attached to + * AwernessMessages they receive only from other users. Therefore instances of this class + * are never considered local. + */ + @Override + public boolean isLocal(){ + return false; + } + + /** + * Returns an instance of {@code DiagramEventActionSource} out of a + * String passed as argument + * @param s a string representation of a {@code DiagramEventActionSource} instance, as + * returned by toString. + * @return an instance of {@code DiagramEventActionSource} + */ + public static DiagramEventActionSource valueOf(String s){ + if(s.isEmpty()) + return NULL; + String[] strings = s.split(SEPARATOR); + long id = Long.parseLong(strings[1]); + String elementName = strings[2]; + long timestamp = Long.parseLong(strings[3]); + DiagramEventSource eventSource = DiagramEventSource.valueOf(strings[5]); + DiagramEventActionSource toReturn = new DiagramEventActionSource(eventSource,Command.Name.valueOf(strings[0]),id,elementName); + toReturn.setUserName(strings[4]); + toReturn.setTimestamp(timestamp); + return toReturn; + } + + /** + * Encodes this object into a String. the encoding is done by concatenating the command name + * with the string representation of the event source. the command name is encoded in a fixed length + * string and padded with white spaces if such length is greater than the command name's. + */ + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(cmd.name()); + builder.append(SEPARATOR); + builder.append(elementID); + builder.append(SEPARATOR); + builder.append(elementName); + builder.append(SEPARATOR); + builder.append(timestamp); + builder.append(SEPARATOR); + builder.append(userName); + builder.append(SEPARATOR); + builder.append(super.toString()); + return builder.toString(); + } + + + + private String userName; + private Command.Name cmd; + private long elementID; + private long saveID; + private String elementName; + private static final String SEPARATOR = "\n"; + private long timestamp; + + public static DiagramEventActionSource NULL = new DiagramEventActionSource(DiagramEventSource.NONE,Command.Name.NONE,-1,""){ + @Override + public String toString(){ + return ""; + } + }; + +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/LockMessage.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/LockMessage.java Wed Apr 25 17:09:09 2012 +0100 @@ -36,8 +36,8 @@ /** * 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); + public LockMessage(Name name, long timestamp, String diagram, Object[] args, Object source) { + super(timestamp, diagram,source); this.args = args; this.name = name; } @@ -46,18 +46,18 @@ * 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); + public LockMessage(Name name, String diagram, Object[] args, DiagramEventActionSource source) { + super(diagram,source); 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, long timestamp, String diagram, long id, DiagramEventActionSource source) { + this(name,timestamp,diagram,new Object[]{id},source); } - public LockMessage(Name name, String diagram, long id){ - this(name,diagram,new Object[]{id}); + public LockMessage(Name name, String diagram, long id, DiagramEventActionSource source){ + this(name,diagram,new Object[]{id},source); } @Override @@ -77,18 +77,24 @@ return args.length; } + @Override + public DiagramEventActionSource getSource(){ + return (DiagramEventActionSource)super.getSource(); + } + public static LockMessage.Name valueOf(String n){ Name name = Name.NONE_L; try { name = Name.valueOf(n); }catch(IllegalArgumentException iae){ - name.setOrigin(n); + iae.printStackTrace(); + return Name.NONE_L; } return name; } /** used to distinguish between different kinds of messages. */ - public static final String LOCK_NAME_POSTFIX = "_L"; + public static final String NAME_POSTFIX = "_L"; public static final String GET_LOCK_PREFIX = "GET_"; public static final String YIELD_LOCK_PREFIX = "YIELD_"; private Name name; @@ -120,22 +126,5 @@ 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; } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/Message.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Message.java Wed Apr 25 17:09:09 2012 +0100 @@ -25,13 +25,14 @@ */ public abstract class Message { - public Message(long timestamp, String diagram){ + public Message(long timestamp, String diagram, Object source){ this.timestamp = timestamp; this.diagram = diagram; + this.source = source; } - public Message(String diagram){ - this(System.currentTimeMillis(),diagram); + public Message(String diagram, Object source){ + this(System.currentTimeMillis(),diagram,source); } public long getTimestamp() { @@ -42,10 +43,19 @@ return diagram; } + public Object getSource(){ + return source; + } + + public void setSource(Object src){ + source = src; + } + public abstract MessageName getName(); private long timestamp; private String diagram; + private Object source; public static interface MessageName { String toString();
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/NetDiagram.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/NetDiagram.java Wed Apr 25 17:09:09 2012 +0100 @@ -30,15 +30,17 @@ 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.DiagramTreeNode; 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.DiagramEventSource; 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.awareness.AwarenessPanel; 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; @@ -58,6 +60,7 @@ private NetDiagram(Diagram delegateDiagram){ this.delegateDiagram = delegateDiagram; + innerModelUpdater = new InnerModelUpdater(); } public static NetDiagram wrapRemoteHost(Diagram diagram, ClientConnectionManager connectionManager,SocketChannel channel){ @@ -117,25 +120,31 @@ return delegateDiagram; } + public abstract void enableAwareness(AwarenessPanel panel); + + public abstract void disableAwareness(AwarenessPanel panel); + public abstract SocketChannel getSocketChannel(); protected abstract void send(Command cmd, DiagramElement element); - protected abstract void send(Command cmd, DiagramModelTreeNode treeNode); + protected abstract void send(Command cmd, DiagramTreeNode treeNode); protected abstract void send(LockMessage lockMessage); + protected abstract void send(AwarenessMessage awMsg); + protected abstract boolean receiveLockAnswer(); private Diagram delegateDiagram; - private InnerModelUpdater innerModelUpdater = new InnerModelUpdater(); + private InnerModelUpdater innerModelUpdater; + public static String LOCALHOST_STRING = " @ localhost"; private class InnerModelUpdater implements DiagramModelUpdater { - @Override - public boolean getLock(DiagramModelTreeNode treeNode, Lock lock) { + public boolean getLock(DiagramTreeNode treeNode, Lock lock, DiagramEventActionSource actionSource) { try { - sendLockMessage(treeNode,lock,true); + sendLockMessage(treeNode,lock,true,actionSource); }catch(IllegalArgumentException iae){ return false; } @@ -143,13 +152,22 @@ } @Override - public void yieldLock(DiagramModelTreeNode treeNode, Lock lock) { + public void yieldLock(DiagramTreeNode treeNode, Lock lock, DiagramEventActionSource actionSource) { try { - sendLockMessage(treeNode,lock,false); + sendLockMessage(treeNode,lock,false,actionSource); }catch(IllegalArgumentException iae) {} } - private void sendLockMessage(DiagramModelTreeNode treeNode, Lock lock, boolean isGettingLock){ + @Override + public void sendAwarenessMessage(AwarenessMessage.Name awMsgName, Object source){ + if(source instanceof DiagramEventActionSource) + send(new AwarenessMessage(awMsgName,getName(),(DiagramEventActionSource)source)); + else if(source instanceof String){ + send(new AwarenessMessage(awMsgName,getName(),(String)source)); + } + } + + private void sendLockMessage(DiagramTreeNode treeNode, Lock lock, boolean isGettingLock, DiagramEventActionSource source){ TreeNode[] path = treeNode.getPath(); Object[] args = new Object[path.length-1]; if(args.length == 0 && !treeNode.isRoot()) @@ -160,22 +178,25 @@ send(new LockMessage( LockMessageConverter.getLockMessageNamefromLock(lock,isGettingLock), delegateDiagram.getName(), - args + args, + source )); } @Override - public void insertInCollection(DiagramElement element) { + public void insertInCollection(DiagramElement element,DiagramEventSource source) { 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()} + new Object[] {element.getType(),bounds.getX(),bounds.getY()}, + makeRemote(source) ); }else{ Edge edge = (Edge)element; @@ -187,7 +208,8 @@ cmd = new Command( Command.Name.INSERT_EDGE, delegateDiagram.getName(), - args + args, + makeRemote(source) ); } send(cmd,element); @@ -195,97 +217,104 @@ @Override public void insertInTree(DiagramElement element) { - insertInCollection(element); + insertInCollection(element,DiagramEventSource.TREE); } @Override - public void takeOutFromCollection(DiagramElement element) { + public void takeOutFromCollection(DiagramElement element,DiagramEventSource source) { 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()} + new Object[] {element.getId()}, + makeRemote(source) ); send(cmd,element); } @Override public void takeOutFromTree(DiagramElement element) { - takeOutFromCollection(element); + takeOutFromCollection(element,DiagramEventSource.TREE); } @Override - public void setName(DiagramElement element, String name) { + public void setName(DiagramElement element, String name, DiagramEventSource source) { send(new Command( element instanceof Node ? Command.Name.SET_NODE_NAME : Command.Name.SET_EDGE_NAME, delegateDiagram.getName(), - new Object[] {element.getId(), name}), + new Object[] {element.getId(), name}, + makeRemote(source)), element); } @Override - public void setNotes(DiagramModelTreeNode treeNode, String notes) { + public void setNotes(DiagramTreeNode treeNode, String notes, DiagramEventSource source) { 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); + Command cmd = new Command(Command.Name.SET_NOTES, delegateDiagram.getName(),args,makeRemote(source)); send(cmd,treeNode); } @Override - public void setProperty(Node node, String type, int index, String value) { + public void setProperty(Node node, String type, int index, String value, DiagramEventSource source) { send(new Command(Command.Name.SET_PROPERTY, delegateDiagram.getName(), - new Object[] {node.getId(),type,index,value}), - node + new Object[] {node.getId(),type,index,value}, + makeRemote(source)), + node ); } @Override - public void setProperties(Node node, NodeProperties properties) { + public void setProperties(Node node, NodeProperties properties, DiagramEventSource source) { send(new Command(Command.Name.SET_PROPERTIES, delegateDiagram.getName(), - new Object[] {node.getId(),properties.toString()}), + new Object[] {node.getId(),properties.toString()}, + makeRemote(source)), node ); } @Override - public void clearProperties(Node node) { + public void clearProperties(Node node, DiagramEventSource source) { send(new Command(Command.Name.CLEAR_PROPERTIES, delegateDiagram.getName(), - new Object[] {node.getId()}), + new Object[] {node.getId()}, + makeRemote(source)), node ); } @Override - public void addProperty(Node node, String type, String value) { + public void addProperty(Node node, String type, String value, DiagramEventSource source) { send(new Command(Command.Name.ADD_PROPERTY, delegateDiagram.getName(), - new Object[] {node.getId(),type,value}), + new Object[] {node.getId(),type,value}, + makeRemote(source)), node ); } @Override - public void removeProperty(Node node, String type, int index) { + public void removeProperty(Node node, String type, int index, DiagramEventSource source) { send(new Command(Command.Name.REMOVE_PROPERTY, delegateDiagram.getName(), - new Object[] {node.getId(),type,index}), + new Object[] {node.getId(),type,index}, + makeRemote(source)), node ); } @Override public void setModifiers(Node node, String type, int index, - Set<Integer> modifiers) { + Set<Integer> modifiers, DiagramEventSource source) { Object args[] = new Object[modifiers.size()+3]; args[0] = node.getId(); args[1] = type; @@ -295,27 +324,27 @@ args[i+3] = I; i++; } - send(new Command(Command.Name.SET_MODIFIERS, delegateDiagram.getName(),args),node); + send(new Command(Command.Name.SET_MODIFIERS, delegateDiagram.getName(),args,makeRemote(source)),node); } @Override - public void setEndLabel(Edge edge, Node node, String label) { + public void setEndLabel(Edge edge, Node node, String label, DiagramEventSource source) { send(new Command(Command.Name.SET_ENDLABEL, delegateDiagram.getName(), - new Object[] {edge.getId(), node.getId(), label}), + new Object[] {edge.getId(), node.getId(), label},makeRemote(source)), edge ); } @Override - public void setEndDescription(Edge edge, Node node, int index) { + public void setEndDescription(Edge edge, Node node, int index, DiagramEventSource source) { send(new Command(Command.Name.SET_ENDDESCRIPTION, delegateDiagram.getName(), - new Object[] {edge.getId(), node.getId(), index}), + new Object[] {edge.getId(), node.getId(), index},makeRemote(source)), edge ); } @Override - public void translate(GraphElement ge, Point2D p, double dx, double dy) { + public void translate(GraphElement ge, Point2D p, double dx, double dy, DiagramEventSource source) { double px = 0; double py = 0; if(p != null){ @@ -325,18 +354,18 @@ 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} + new Object[] {n.getId(), px, py, dx,dy},makeRemote(source) ),n); }else{ Edge e = (Edge)ge; send(new Command(Command.Name.TRANSLATE_EDGE, delegateDiagram.getName(), - new Object[] {e.getId(), px, py, dx,dy} + new Object[] {e.getId(), px, py, dx,dy},makeRemote(source) ),e); } } @Override - public void startMove(GraphElement ge, Point2D p) { + public void startMove(GraphElement ge, Point2D p, DiagramEventSource source) { /* 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 * @@ -346,35 +375,46 @@ } @Override - public void bend(Edge edge, Point2D p) { + public void bend(Edge edge, Point2D p, DiagramEventSource source) { /* 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); + new Object[] {edge.getId(),p.getX(),p.getY()}, + makeRemote(source)), + edge); else{ send(new Command(Command.Name.BEND, delegateDiagram.getName(), - new Object[] {edge.getId(),p.getX(),p.getY(),edgeStartMovePoint.getX(),edgeStartMovePoint.getY()}), + new Object[] {edge.getId(),p.getX(),p.getY(), + edgeStartMovePoint.getX(),edgeStartMovePoint.getY()}, + makeRemote(source)), edge); edgeStartMovePoint = null; } } @Override - public void stopMove(GraphElement ge) { + public void stopMove(GraphElement ge, DiagramEventSource source) { if(ge instanceof Node){ Node n = (Node)ge; send(new Command(Command.Name.STOP_NODE_MOVE, delegateDiagram.getName(), - new Object[] {n.getId()}), + new Object[] {n.getId()},makeRemote(source)), n); }else{ Edge e = (Edge)ge; send(new Command(Command.Name.STOP_EDGE_MOVE, delegateDiagram.getName(), - new Object[] {e.getId()}), + new Object[] {e.getId()},makeRemote(source)), e); } } + /* source passed as argument to the updater methods have are local and with no id + * since this source has to be sent to the server, it must be set as non local + * (constructor will do) and the is must be set as well + */ + private DiagramEventSource makeRemote(DiagramEventSource src){ + return new DiagramEventSource(src); + } + private Point2D edgeStartMovePoint; } @@ -400,7 +440,7 @@ } @Override - protected void send(Command cmd, DiagramModelTreeNode treeNode) { + protected void send(Command cmd, DiagramTreeNode treeNode) { connectionManager.addRequest(new ClientConnectionManager.SendTreeCmdRequest(cmd, channel, treeNode )); } @@ -410,10 +450,17 @@ } @Override + protected void send(AwarenessMessage awMsg){ + connectionManager.addRequest(new ClientConnectionManager.SendAwarenessRequest(channel,awMsg)); + } + + @Override protected boolean receiveLockAnswer(){ ClientConnectionManager.Answer answer = connectionManager.getAnswer(); - if(answer instanceof ClientConnectionManager.ErrorAnswer) - return false; + /* diagram has been reverted while waiting for a lock answer : the answer is gonna be yes * + * then, as the client is no longer connected to the server and there is no more locking in place */ + if(answer instanceof ClientConnectionManager.RevertedDiagramAnswer) + return true; LockMessage.Name name = ((LockAnswer)answer).message.getName(); switch(name){ case YES_L : @@ -439,6 +486,16 @@ } @Override + public void enableAwareness(AwarenessPanel panel){ + connectionManager.getAwarenessPanelEditor().addAwarenessPanel(panel); + } + + @Override + public void disableAwareness(AwarenessPanel panel){ + connectionManager.getAwarenessPanelEditor().removeAwarenessPanel(panel); + } + + @Override public Object clone(){ throw new UnsupportedOperationException(); } @@ -489,7 +546,16 @@ } catch (IOException ioe) { exceptionHandler.handleException(ioe); } - } + } + + @Override + protected void send(AwarenessMessage awMsg){ + try { + protocol.send(channel, awMsg); + } catch (IOException ioe) { + exceptionHandler.handleException(ioe); + } + } @Override protected boolean receiveLockAnswer(){ @@ -511,7 +577,7 @@ } @Override - protected void send(Command cmd , DiagramModelTreeNode treeNode){ + protected void send(Command cmd , DiagramTreeNode treeNode){ try { protocol.send(channel, cmd); } catch (IOException ioe) { @@ -521,7 +587,7 @@ @Override public String getLabel(){ - return getName()+" @ localhost"; + return getName()+LOCALHOST_STRING; } @Override @@ -529,8 +595,18 @@ return channel; } - SocketChannel channel; - Queue<DiagramElement> diagramElements; + @Override + public void enableAwareness(AwarenessPanel panel){ + Server.getServer().getAwarenessPanelEditor().addAwarenessPanel(panel); + } + + @Override + public void disableAwareness(AwarenessPanel panel){ + Server.getServer().getAwarenessPanelEditor().removeAwarenessPanel(panel); + } + + private SocketChannel channel; + private 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/NetworkThread.java Wed Apr 25 17:09:09 2012 +0100 @@ -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.network; + +import uk.ac.qmul.eecs.ccmi.gui.awareness.AwarenessPanelEditor; + +class NetworkThread extends Thread { + + public NetworkThread() { + super(); + panelEditor = new AwarenessPanelEditor(); + } + + public NetworkThread(String name) { + super(name); + panelEditor = new AwarenessPanelEditor(); + } + + public AwarenessPanelEditor getAwarenessPanelEditor(){ + return panelEditor; + } + + private AwarenessPanelEditor panelEditor; +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/OscProtocol.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/OscProtocol.java Wed Apr 25 17:09:09 2012 +0100 @@ -23,6 +23,9 @@ import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; +import java.util.ResourceBundle; + +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; import de.sciss.net.OSCBundle; import de.sciss.net.OSCMessage; @@ -43,16 +46,19 @@ 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()]; + Object[] args = new Object[2+cmd.getArgNum()]; args[0] = cmd.getDiagram(); + args[1] = cmd.getSource().toString(); for(int i=0; i<cmd.getArgNum();i++) - args[i+1] = cmd.getArgAt(i); + args[i+2] = 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); + throw new IOException( + /* give a more user friendly message */ + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.no_send"),u); } } @@ -62,56 +68,77 @@ bundle = new CCmIOSCBundle(reply.getTimestamp()); bundle.addPacket(new OSCMessage( OSC_NAME_PREFIX+reply.getName().toString(), - new Object[] {reply.getMessageLen(), reply.getDiagram() ,reply.getMessage()} - ) - ); + new Object[] {reply.getMessageLen(), reply.getDiagram() ,reply.getMessage(),reply.getSource().toString()} + )); try{ writeBundle(channel); }catch(IOException u){ - throw new IOException("Could not send data to the server",u); + throw new IOException( + /* give a more user friendly message */ + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.no_send"),u); } } @Override public void send(SocketChannel channel, LockMessage lockMessage) throws IOException { - /* OSC message args = [diagram, lock.arg1, lock.arg2, lock.arg2, ... , lock.argN ] */ + /* OSC message args = [diagram, source, lock.arg1, lock.arg2, lock.arg2, ... , lock.argN ] */ bundle = new CCmIOSCBundle(lockMessage.getTimestamp()); - Object[] args = new Object[1+lockMessage.getArgNum()]; + Object[] args = new Object[2+lockMessage.getArgNum()]; args[0] = lockMessage.getDiagram(); + args[1] = lockMessage.getSource().toString(); for(int i=0; i<lockMessage.getArgNum();i++) - args[i+1] = lockMessage.getArgAt(i); + args[i+2] = lockMessage.getArgAt(i); bundle.addPacket(new OSCMessage( OSC_NAME_PREFIX+lockMessage.getName().toString(), - args - ) - ); + args)); try{ writeBundle(channel); }catch(IOException u){ - /* give a more user friendly message */ - throw new IOException("Could not send data to the server",u); + throw new IOException( + /* give a more user friendly message */ + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.no_send"),u); + } + } + + @Override + public void send(SocketChannel channel, AwarenessMessage awareMsg) throws IOException { + /* OSC message args = [diagram name, siagram event action source]*/ + bundle = new CCmIOSCBundle(awareMsg.getTimestamp()); + Object[] args = new Object[2]; + args[0] = awareMsg.getDiagram(); + args[1] = awareMsg.getSource().toString(); + + bundle.addPacket(new OSCMessage( + OSC_NAME_PREFIX+awareMsg.getName().toString(), + args)); + try{ + writeBundle(channel); + }catch(IOException u){ + throw new IOException( + /* give a more user friendly message */ + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.no_send"),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 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 '/' +// Object args[] = new Object[oscMessage.getArgCount()-CMD_OFFSET]; +// for(int i=0; i< args.length;i++) +// args[i] = oscMessage.getArg(i+CMD_OFFSET); +// return new Command( +// Command.valueOf(name), +// (String)oscMessage.getArg(CMD_DIAGRAM_INDEX), +// args, +// bundle.getTimeTag(), +// DiagramEventSource.valueOf((String)oscMessage.getArg(CMD_SOURCE_INDEX)) +// ); +// } @Override public Reply receiveReply(SocketChannel channel) throws IOException { @@ -126,7 +153,8 @@ Reply.valueOf(name), (String)oscMessage.getArg(REPLY_DIAGRAM_INDEX), (String)oscMessage.getArg(REPLY_MESSAGE_INDEX), - bundle.getTimeTag()); + bundle.getTimeTag(), + DiagramEventSource.valueOf((String)oscMessage.getArg(REPLY_SOURCE_INDEX))); } @Override @@ -135,15 +163,15 @@ 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; + Object args[] = new Object[oscMessage.getArgCount()-LOCK_OFFSET]; for(int i=0; i< args.length;i++) - args[i] = oscMessage.getArg(i+offset); + args[i] = oscMessage.getArg(i+LOCK_OFFSET); return new LockMessage( LockMessage.valueOf(name), bundle.getTimeTag(), (String)oscMessage.getArg(LOCK_DIAGRAM_INDEX), - args + args, + DiagramEventActionSource.valueOf((String)oscMessage.getArg(LOCK_SOURCE_INDEX)) ); } @@ -153,36 +181,50 @@ 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 + if(name.endsWith(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()); + bundle.getTimeTag(), + DiagramEventSource.valueOf((String)oscMessage.getArg(REPLY_SOURCE_INDEX))); return reply; - }else if (name.endsWith(LockMessage.LOCK_NAME_POSTFIX)){ - Object args[] = new Object[oscMessage.getArgCount()-1]; - int offset = LOCK_DIAGRAM_INDEX + 1; + }else if (name.endsWith(LockMessage.NAME_POSTFIX)){ + Object args[] = new Object[oscMessage.getArgCount()-LOCK_OFFSET]; for(int i=0; i< args.length;i++) - args[i] = oscMessage.getArg(i+offset); + args[i] = oscMessage.getArg(i+LOCK_OFFSET); return new LockMessage( LockMessage.valueOf(name), bundle.getTimeTag(), (String)oscMessage.getArg(LOCK_DIAGRAM_INDEX), - args + args, + DiagramEventActionSource.valueOf((String)oscMessage.getArg(LOCK_SOURCE_INDEX)) ); + }else if(name.endsWith(AwarenessMessage.NAME_POSTFIX)){ // it's an awareness message + AwarenessMessage.Name awName = AwarenessMessage.valueOf(name); + if(awName == AwarenessMessage.Name.USERNAME_A || awName == AwarenessMessage.Name.ERROR_A){ + return new AwarenessMessage(awName, + (String)oscMessage.getArg(AWAR_DIAGRAM_INDEX), + (String)oscMessage.getArg(AWAR_SOURCE_INDEX) + ); + }else { + return new AwarenessMessage(awName, + (String)oscMessage.getArg(AWAR_DIAGRAM_INDEX), + DiagramEventActionSource.valueOf((String)oscMessage.getArg(AWAR_SOURCE_INDEX)) + ); + } }else{ // it's a command - Object args[] = new Object[oscMessage.getArgCount()-1]; - int offset = CMD_DIAGRAM_INDEX + 1; + Object args[] = new Object[oscMessage.getArgCount()-CMD_OFFSET]; for(int i=0; i< args.length;i++) - args[i] = oscMessage.getArg(i+offset); + args[i] = oscMessage.getArg(i+CMD_OFFSET); return new Command( Command.valueOf(name), (String)oscMessage.getArg(CMD_DIAGRAM_INDEX), args, - bundle.getTimeTag() + bundle.getTimeTag(), + DiagramEventSource.valueOf((String)oscMessage.getArg(CMD_SOURCE_INDEX)) ); } } @@ -192,7 +234,7 @@ buffer.rewind().limit(4); while( buffer.hasRemaining() ) if( channel.read( buffer ) == -1 ) - throw new SocketException("Connection closed by peer"); + throw new SocketException(ResourceBundle.getBundle(Server.class.getName()).getString("error.connection_close")); buffer.rewind(); int packetSize = buffer.getInt(); @@ -206,7 +248,7 @@ /* 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"); + throw new SocketException(ResourceBundle.getBundle(Server.class.getName()).getString("error.connection_close")); b.rewind(); return (OSCBundle)codec.decode(b); } @@ -236,8 +278,15 @@ 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 REPLY_SOURCE_INDEX = 3; private static final int CMD_DIAGRAM_INDEX = 0; + private static final int CMD_SOURCE_INDEX = 1; + private static final int CMD_OFFSET = 2; private static final int LOCK_DIAGRAM_INDEX = 0; + private static final int LOCK_SOURCE_INDEX = 1; + private static final int LOCK_OFFSET = 2; + private static final int AWAR_DIAGRAM_INDEX = 0; + private static final int AWAR_SOURCE_INDEX = 1; /* ------------------------------------------------*/ private static final int REPLY_MESSAGE_INDEX = 2; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/Protocol.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Protocol.java Wed Apr 25 17:09:09 2012 +0100 @@ -34,10 +34,10 @@ void send(SocketChannel channel, LockMessage lock) throws IOException; + void send(SocketChannel channel, AwarenessMessage awareMsg) 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;
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/Reply.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Reply.java Wed Apr 25 17:09:09 2012 +0100 @@ -19,6 +19,7 @@ package uk.ac.qmul.eecs.ccmi.network; +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; /** @@ -31,14 +32,14 @@ */ public class Reply extends Message { - public Reply(Name name, String diagram, String message, long timestamp){ - super(timestamp,diagram); + public Reply(Name name, String diagram, String message, long timestamp, DiagramEventSource source){ + super(timestamp,diagram, source); this.name = name; this.message = message; } - public Reply(Name name, String diagram, String message){ - super(diagram); + public Reply(Name name, String diagram, String message, DiagramEventSource source){ + super(diagram,source); this.name = name; this.message = message; } @@ -51,6 +52,11 @@ return message; } + @Override + public DiagramEventSource getSource(){ + return (DiagramEventSource)super.getSource(); + } + /** * @return the length of the String message conveyed by this Reply */ @@ -70,7 +76,7 @@ try { name = Name.valueOf(n); }catch(IllegalArgumentException iae){ - name.setOrigin(n); + iae.printStackTrace(); } return name; } @@ -87,7 +93,7 @@ private Name name; private String message; private long timestamp; - public static final String REPLY_NAME_POSTFIX = "_R"; + public static final String NAME_POSTFIX = "_R"; public static enum Name implements Message.MessageName { NONE_R, OK_R, @@ -114,22 +120,5 @@ 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; }; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/Server.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Server.java Wed Apr 25 17:09:09 2012 +0100 @@ -34,13 +34,11 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Handler; import java.util.logging.Level; +import java.util.logging.LogRecord; 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; @@ -57,8 +55,14 @@ * and wait for an acknowledging reply. * */ -public class Server extends Thread { +public class Server extends NetworkThread { + /** + * Create a new server instance. If another instance is already running then it's shut + * down before the new instance is created. + * + * @return a {@Server} instance. + */ public static Server createServer(){ if(server != null) server.shutdown(server.resources.getString("log.restart")); @@ -66,6 +70,10 @@ return server; } + public static Server getServer(){ + return server; + } + private Server(){ super("Server Thread"); mustSayGoodbye = false; @@ -73,19 +81,32 @@ resources = ResourceBundle.getBundle(this.getClass().getName()); } - public void init(JFrame frame) throws IOException { + public void init() throws IOException { if(initialized) return; serverChannel = ServerSocketChannel.open(); selector = Selector.open(); diagrams = new HashMap<String,Diagram>(); - scManager = new ServerConnectionManager(diagrams); + scManager = new ServerConnectionManager(diagrams,getAwarenessPanelEditor()); 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)); + Handler serverLogHandler = new Handler(){ + @Override + public void close() throws SecurityException {} + + @Override + public void flush() {} + + @Override + public void publish(LogRecord record) { + NarratorFactory.getInstance().speakWholeText(record.getMessage()); + } + }; + serverLogHandler.setLevel(Level.CONFIG); + logger.addHandler(serverLogHandler); logger.config("Server initialized, will listen on port: "+port); initialized = true; } @@ -109,6 +130,9 @@ SocketChannel clientChannel = serverChannel.accept(); clientChannel.configureBlocking(false); clientChannel.register(selector, SelectionKey.OP_READ); + /* log connection only if it's not from the local channel */ + if(!"/127.0.0.1".equals(clientChannel.socket().getInetAddress().toString())) + logger.info(resources.getString("log.client_connected")); } if(key.isReadable()){ try{ @@ -118,7 +142,7 @@ SocketChannel channel = (SocketChannel)key.channel(); channel.close(); scManager.removeChannel(channel); - logger.severe(ioe.getLocalizedMessage()); + logger.info(ioe.getLocalizedMessage()); } } } @@ -194,7 +218,7 @@ private boolean running; static { - logger = Logger.getLogger(Server.class.getCanonicalName()); + logger = Logger.getLogger(Server.class.getName()); logger.setUseParentHandlers(false); logger.setLevel(Level.CONFIG); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/Server.properties Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Server.properties Wed Apr 25 17:09:09 2012 +0100 @@ -2,8 +2,14 @@ log.start=Server started... log.restart=Restarting server... log.shutdown=Server shutdown: +log.client_connected=Peer connected to local server 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 +dialog.error.connections=A network problem occurred. All diagrams are no longer shared +dialog.error.no_send=Could not send data to the server + +awareness.msg.user_already_exists=Selected User Name already in use + +error.connection_close=Connection with peer closed \ No newline at end of file
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/ServerConnectionManager.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ServerConnectionManager.java Wed Apr 25 17:09:09 2012 +0100 @@ -25,64 +25,94 @@ 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.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.ResourceBundle; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; 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.diagrammodel.DiagramTreeNode; import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; 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.awareness.AwarenessPanelEditor; +import uk.ac.qmul.eecs.ccmi.gui.awareness.BroadcastFilter; +import uk.ac.qmul.eecs.ccmi.gui.awareness.DisplayFilter; import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; - - +import uk.ac.qmul.eecs.ccmi.speech.Narrator; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; /* 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){ + ServerConnectionManager( Map<String,Diagram> diagrams, AwarenessPanelEditor panelEditor) { this.diagrams = diagrams; - diagramChannelAllocations = new HashMap<Diagram,Set<SocketChannel>>(); + awarenessPanelEditor = panelEditor; + diagramChannelAllocations = new HashMap<Diagram,Set<UserNameSocketChannel>>(); localhostDiagramElementQueue = new ConcurrentHashMap<String,Queue<DiagramElement>>(); protocol = ProtocolFactory.newInstance(); lockManager = new ServerLockManager(); + broadcastFilter = BroadcastFilter.getInstance(); } /** - * remove the channel and the locks related to it from the inner data structures + * Removes the channel and the locks related to it from the inner data structures. + * * @param channel the channel to remove */ - public void removeChannel(SocketChannel channel){ + void removeChannel(SocketChannel channel) throws IOException{ String diagramName = null; - for(Map.Entry<Diagram,Set<SocketChannel>> entry : diagramChannelAllocations.entrySet()){ - if(entry.getValue().remove(channel)){ + /* looks for the Set containing this channel */ + for(Map.Entry<Diagram,Set<UserNameSocketChannel>> entry : diagramChannelAllocations.entrySet()){ + UserNameSocketChannel unsc = null; + for(UserNameSocketChannel userNameSocketChannel : entry.getValue()) + if(userNameSocketChannel.channel.equals(channel)){ + unsc = userNameSocketChannel; + break; + } + /* remove the channel from this set of channels */ + if(entry.getValue().remove(unsc) && !unsc.userName.isEmpty()){ diagramName = entry.getKey().getName(); + awarenessPanelEditor.removeUserName(diagramName, unsc.userName); + /* notify the other clients the user has disconnected */ + AwarenessMessage awMsg = new AwarenessMessage( + AwarenessMessage.Name.USERNAME_A, + diagramName, + ""+AwarenessMessage.USERNAMES_SEPARATOR+unsc.userName + ); + for(UserNameSocketChannel userNameSocketChannel : entry.getValue()){ + protocol.send(userNameSocketChannel.channel, awMsg); + } + break; } + } + /* all locks held by disconnected user are released */ lockManager.removeLocks(channel, diagramName); } - public void handleMessage(SocketChannel channel) throws IOException{ + void handleMessage(SocketChannel channel) throws IOException{ Message message = protocol.receiveMessage(channel); if(message instanceof Command){ handleCommand((Command)message, channel); - }else { + }else if(message instanceof LockMessage) { handleLockMessage((LockMessage)message,channel); + }else { // awareness message - broadcast the message + handleAwarenessMessage((AwarenessMessage)message,channel,null); } } @@ -91,16 +121,30 @@ String name = lockMessage.getName().toString(); String diagramName = lockMessage.getDiagram(); + Diagram diagram = null; + synchronized(diagrams){ + diagram = diagrams.get(diagramName); + } + if(diagram == null){ + replyLockMessage(channel,diagramName,false); + return; + } + /* 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()); - } + + DiagramTreeNode treeNode = null; + /* synchronize with the event dispatching thread */ + ReentrantLock monitor = diagram.getCollectionModel().getMonitor(); + monitor.lock(); + + treeNode = Finder.findTreeNode(path, (DiagramTreeNode)diagram.getTreeModel().getRoot()); /* the tree node has been deleted, lock cannot be granted */ if(treeNode == null){ + monitor.unlock(); replyLockMessage(channel,diagramName,false); return; } @@ -108,61 +152,139 @@ /* 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); + // System.out.println("get lock source:"+ lockMessage.getSource()); + boolean succeeded; + succeeded = lockManager.requestLock(treeNode, lock, channel, diagramName); + monitor.unlock(); + /* send the response */ + replyLockMessage(channel,diagramName,succeeded); + if(succeeded && broadcastFilter.accept(lockMessage.getSource())){ + DiagramEventActionSource processedSource = broadcastFilter.process(lockMessage.getSource()); // changes according to configuration; + + //select node is a temporary record, therefore it doesn't need to be stored + if(processedSource.getCmd() != Command.Name.SELECT_NODE_FOR_EDGE_CREATION){ + Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram); + /* saves the diagramEventActionSource for when the lock is yielded */ + if(userNames != null){ + for(UserNameSocketChannel userName : userNames){ + if(userName.channel.equals(channel)){ + userName.lockAwarenessSources.add(processedSource); + } + } + } + } + /* handle the awareness message piggybacked in the lock message */ + AwarenessMessage awarMsg = new AwarenessMessage( + AwarenessMessage.Name.START_A, + diagramName, + processedSource + ); + handleAwarenessMessage(awarMsg,channel,diagram); + } }else{ // yield lock - lockManager.releaseLock(treeNode, lock, channel,diagramName); + boolean released = lockManager.releaseLock(treeNode, lock, channel,diagramName); + monitor.unlock(); + DiagramEventActionSource source = lockMessage.getSource(); + /* it's NULL for NOTES lock and SELECT_NODE_FOR_EDGE_CREATION must not clean the text panel, because its record is temporary */ + if(released && source != DiagramEventActionSource.NULL && source.getCmd() != Command.Name.SELECT_NODE_FOR_EDGE_CREATION){ + + if(source.getCmd() == Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION){ + /* unselect node for edge creation is treated differently because it doesn't * + * clear the text panel but adds another record, which is temporary */ + handleAwarenessMessage(new AwarenessMessage( + AwarenessMessage.Name.STOP_A, + diagramName, + source), + channel, + diagram); + return; + } + + /* retrieves the diagramEventActionSource: when the lock was gotten, the source was stored in * + * userName.lockAwarenessSource. This is done because the broadcast filter configuration might * + * have changed in the meanwhile but we still need to send the aw msg with the original source * + * or the clients won't be able to pick out the string to delete */ + DiagramEventActionSource savedSource = removeEventActionSource(channel,source.getSaveID(),diagram); + + /* saved source = null means the broadcast filter didn't let the get_lock message + * this yield_lock message is referring to. Move on */ + if(savedSource == null){ + return; + } + + AwarenessMessage awMsg = new AwarenessMessage( + AwarenessMessage.Name.STOP_A, + diagramName, + savedSource + ); + handleAwarenessMessage(awMsg,channel,diagram); + } } } 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); + Diagram diagram = null; + + if(cmd.getName() != Command.Name.LIST){ + synchronized(diagrams){ + diagram = diagrams.get(diagramName); + } + if(diagram == null) + protocol.send(channel, new Reply(Reply.Name.ERROR_R,diagramName,"Diagram "+diagramName+" does not exists",cmd.getSource())); } - node = null; - edge = null; + Node node = null; + Edge edge = null; boolean broadcast = true; - + + DiagramEventSource source = cmd.getSource(); + if(channel == localChannel) + source = source.getLocalSource(); + /* set the diagram id so the haptic will update the diagram specified by the command and not the active tab's */ + if(diagram != null) + source.setDiagramName(diagram.getName()); /* 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 : + case LOCAL : // the local socket makes itself known to the server localChannel = channel; - Set<SocketChannel> list = new HashSet<SocketChannel>(); - list.add(localChannel); + Set<UserNameSocketChannel> list = new HashSet<UserNameSocketChannel>(); + list.add(new UserNameSocketChannel(localChannel,AwarenessMessage.getDefaultUserName())); diagramChannelAllocations.put(diagram, list); broadcast = false; break; - case LIST : + case LIST : // ask for the list of available diagrams on the server 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())); + protocol.send(channel, new Reply(Reply.Name.LIST_R,"",names.toString(),DiagramEventSource.NONE)); broadcast = false; break; - case GET : + case GET : // ask for a diagram xml try{ - if(diagram == null){ - protocol.send(channel, new Reply(Reply.Name.ERROR_R,diagramName,"Diagram "+diagramName+" does not exists")); - break; - } + diagram.getCollectionModel().getMonitor().lock(); + Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram); ByteArrayOutputStream out = new ByteArrayOutputStream(); PersistenceManager.encodeDiagramInstance(diagram, new BufferedOutputStream(out)); + diagram.getCollectionModel().getMonitor().unlock(); try{ - protocol.send(channel, new Reply(Reply.Name.GET_R,diagramName,out.toString("UTF-8"))); + protocol.send(channel, new Reply(Reply.Name.GET_R,diagramName,out.toString("UTF-8"),DiagramEventSource.NONE)); + for(UserNameSocketChannel sc: userNames){ + protocol.send(channel, new AwarenessMessage(AwarenessMessage.Name.USERNAME_A,diagramName,sc.userName)); + } }catch(IOException ioe){ throw ioe; } - diagramChannelAllocations.get(diagram).add(channel); + userNames.add(new UserNameSocketChannel(channel)); broadcast = false; }catch(Exception e){ - // log and discard the packet + // close the socket, log and discard the packet + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();} Server.logger.severe(e.getMessage()); } break; @@ -174,71 +296,142 @@ }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); + 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(),DiagramEventSource.NONE); + /* perform the actual translation from the origin */ + node.translate(new Point2D.Double(), dx, dy, DiagramEventSource.NONE); + } + /* wait for the node to be inserted in the model so that it gets an id, which is then * + * used in the awareness message to notify other users that the node has been inserted */ + try { + SwingUtilities.invokeAndWait(new CommandExecutor.Insert(diagram.getCollectionModel(),node,null,source)); + } 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.INSERT_NODE_R,diagramName,"Insert new Node",source.getLocalSource())); + + DiagramEventActionSource actionSource = new DiagramEventActionSource(source, + Command.Name.INSERT_NODE,node.getId(),node.getName()); + if(broadcastFilter.accept(actionSource)){ + /* must set the username to the one of the client who sent the command * + * otherwise the local username is automatically assigned in the constructor */ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel.equals(channel)) + actionSource.setUserName(sc.userName); + } + /* process on the broadcast filter */ + DiagramEventActionSource processedSource = broadcastFilter.process(actionSource); + /* since no lock is used we must send an awareness message without piggybacking */ + AwarenessMessage awMsg = new AwarenessMessage( + AwarenessMessage.Name.START_A, + diagramName, + processedSource + ); + if(channel != localChannel){ + /* update the local awareness panel and speech */ + awarenessPanelEditor.addTimedRecord(awMsg.getDiagram(), DisplayFilter.getInstance().processForText(processedSource)); + NarratorFactory.getInstance().speakWholeText(DisplayFilter.getInstance().processForSpeech(processedSource), Narrator.SECOND_VOICE); + } + /* broadcast the awareness message to all the clients but one which sent the * + * command and the local one to inform them the action has started */ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(!sc.channel.equals(channel) && !sc.channel.equals(localChannel)){ + protocol.send(sc.channel, awMsg); + } } } - 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(); + lockManager.removeLocks(node, diagramName); }else{ - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + lockManager.removeLocks(node, diagramName); + diagram.getCollectionModel().getMonitor().unlock(); } + /* remove the action source, like when a lock is yielded, for this node */ + removeEventActionSource(channel,node.getId(),diagram); /* 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)); + SwingUtilities.invokeAndWait(new CommandExecutor.Remove(diagram.getCollectionModel(),node,source)); } 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")); + protocol.send(channel, new Reply(Reply.Name.REMOVE_NODE_R,diagramName,"Node with id "+ node.getId() +" removed",source.getLocalSource())); break; case INSERT_EDGE : + long[] nodesToConnect = null; 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 + edge = Finder.findEdge((String)cmd.getArgAt(0),diagram.getEdgePrototypes()); + edge = (Edge)edge.clone(); + nodesToConnect = new long[cmd.getArgNum()-1]; + for(int i=0;i<nodesToConnect.length;i++){ + nodesToConnect[i] = (Long)cmd.getArgAt(i+1); + } + } + /* wait for the edge to be inserted in the model so that it gets an id, which is then * + * used in the awareness message to notify other users that the node has been inserted */ + try { + SwingUtilities.invokeAndWait(new CommandExecutor.Insert(diagram.getCollectionModel(), edge, nodesToConnect, source)); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + /* 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", source.getLocalSource())); + + /* send the awareness message for edge insertion */ + DiagramEventActionSource actSource = new DiagramEventActionSource(source, + Command.Name.INSERT_EDGE,edge.getId(),edge.getName()); + if(broadcastFilter.accept(actSource)){ + /* must set the username to the one of the client who sent the command * + * otherwise the local username is automatically assigned in the constructor */ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel.equals(channel)) + actSource.setUserName(sc.userName); + } + + /* process it with the broadcast filter */ + DiagramEventActionSource processedSource = broadcastFilter.process(actSource); + + /* since no lock is used we must send an awareness message without piggybacking */ + AwarenessMessage awMsg = new AwarenessMessage( + AwarenessMessage.Name.START_A, + diagramName, + processedSource + ); + + if(channel != localChannel){ + /* update the local awareness panel and speech */ + awarenessPanelEditor.addTimedRecord(awMsg.getDiagram(), DisplayFilter.getInstance().processForText(processedSource)); + NarratorFactory.getInstance().speakWholeText(DisplayFilter.getInstance().processForSpeech(processedSource), Narrator.SECOND_VOICE); + } + /* broadcast the awareness message to all the clients but one which sent the * + * command and the local one to inform them the action has started */ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(!sc.channel.equals(channel) && !sc.channel.equals(localChannel)){ + protocol.send(sc.channel, awMsg); } } } - 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){ @@ -246,178 +439,194 @@ /* 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()); - } + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); } /* 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)); + SwingUtilities.invokeAndWait(new CommandExecutor.Remove(diagram.getCollectionModel(), edge, source)); } catch (Exception e) { throw new RuntimeException(e); // must never happen } + /* remove the action source, like when a lock is yielded, for this node */ + removeEventActionSource(channel,edge.getId(),diagram); /* 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")); + protocol.send(channel, new Reply(Reply.Name.REMOVE_EDGE_R,diagramName,"Edge with id "+ edge.getId() +" removed", source.getLocalSource())); 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)))); + case SET_EDGE_NAME : { + DiagramElement de = null; + diagram.getCollectionModel().getMonitor().lock(); + de = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetName(de,((String)cmd.getArgAt(1)),source)); 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)))); + protocol.send(channel, new Reply(Reply.Name.SET_EDGE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1),source.getLocalSource())); + }break; + case SET_NODE_NAME : { + DiagramElement de = null; + diagram.getCollectionModel().getMonitor().lock(); + de = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetName(de,((String)cmd.getArgAt(1)),source)); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.SET_NODE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1))); - break; + protocol.send(channel, new Reply(Reply.Name.SET_NODE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1),source.getLocalSource())); + }break; case SET_PROPERTY : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.SetProperty( node, (String)cmd.getArgAt(1), (Integer)cmd.getArgAt(2), - (String)cmd.getArgAt(3) + (String)cmd.getArgAt(3), + source )); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.SET_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(2)+ " set to "+ cmd.getArgAt(3))); + protocol.send(channel, new Reply(Reply.Name.SET_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(2)+ " set to "+ cmd.getArgAt(3),source.getLocalSource())); 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)); + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetProperties( node, - properties + (String)cmd.getArgAt(1), + source )); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.SET_PROPERTIES_R,diagramName,"Properties for " + node.getName()+ " set to "+ cmd.getArgAt(1))); + protocol.send(channel, new Reply(Reply.Name.SET_PROPERTIES_R,diagramName,"Properties for " + node.getName()+ " set to "+ cmd.getArgAt(1),source.getLocalSource())); break; case CLEAR_PROPERTIES : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } - SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node)); + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node,source)); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.CLEAR_PROPERTIES_R,diagramName,"Propertis of Node "+ node.getName() +" cleared")); + protocol.send(channel, new Reply(Reply.Name.CLEAR_PROPERTIES_R,diagramName,"Propertis of Node "+ node.getName() +" cleared",source.getLocalSource())); break; - case SET_NOTES : + case SET_NOTES :{ + DiagramTreeNode treeNode = null; 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)); + diagram.getCollectionModel().getMonitor().lock(); + treeNode = Finder.findTreeNode(path, (DiagramTreeNode)diagram.getTreeModel().getRoot()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetNotes(diagram.getTreeModel(),treeNode,notes,source)); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.SET_NOTES_R,diagramName,"Notes for " + treeNode.getName() + " successfully updated")); - break; + protocol.send(channel, new Reply(Reply.Name.SET_NOTES_R,diagramName,"Notes for " + treeNode.getName() + " successfully updated",source.getLocalSource())); + }break; case ADD_PROPERTY : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.AddProperty( node, (String)cmd.getArgAt(1), - (String)cmd.getArgAt(2) + (String)cmd.getArgAt(2), + source )); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.ADD_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " added to "+ cmd.getArgAt(1))); + protocol.send(channel, new Reply(Reply.Name.ADD_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " added to "+ cmd.getArgAt(1),source.getLocalSource())); break; case REMOVE_PROPERTY : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( node, (String)cmd.getArgAt(1), - (Integer)cmd.getArgAt(2))); + (Integer)cmd.getArgAt(2), + source)); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.REMOVE_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " of type "+ cmd.getArgAt(1)+" removed")); + protocol.send(channel, new Reply(Reply.Name.REMOVE_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " of type "+ cmd.getArgAt(1)+" removed",source.getLocalSource())); 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)); - } + diagram.getCollectionModel().getMonitor().lock(); + 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)); } + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.SetModifiers( node, (String)cmd.getArgAt(1), (Integer)cmd.getArgAt(2), - indexes)); + indexes, + source)); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.SET_MODIFIERS_R,diagramName,"Modifiers for " + cmd.getArgAt(1)+ " successfully set")); + protocol.send(channel, new Reply(Reply.Name.SET_MODIFIERS_R,diagramName,"Modifiers for " + cmd.getArgAt(1)+ " successfully set",source.getLocalSource())); 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))); + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel(edge,node,(String)cmd.getArgAt(2),source)); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.SET_ENDLABEL_R,diagramName,"Endlabel set to "+ cmd.getArgAt(2) +" for edge "+edge.getName())); + protocol.send(channel, new Reply(Reply.Name.SET_ENDLABEL_R,diagramName,"Endlabel set to "+ cmd.getArgAt(2) +" for edge "+edge.getName(),source.getLocalSource())); 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()); - } + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription( edge, node, - (Integer)cmd.getArgAt(2) + (Integer)cmd.getArgAt(2), + source )); 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())); + +(cmd.getArgNum() == 3 ? cmd.getArgAt(2) : Edge.NO_ENDDESCRIPTION_STRING) + + " for edge",source.getLocalSource())); break; case TRANSLATE_NODE : - synchronized(diagram.getCollectionModel().getMonitor()){ - node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); - } + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); 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) + (Double)cmd.getArgAt(4), + source )); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.TRANSLATE_NODE_R,diagramName,"Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")")); + protocol.send(channel, new Reply(Reply.Name.TRANSLATE_NODE_R,diagramName,"Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")",source.getLocalSource())); break; case TRANSLATE_EDGE : - synchronized(diagram.getCollectionModel().getMonitor()){ - edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); - } + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); 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) + (Double)cmd.getArgAt(4), + source )); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.TRANSLATE_EDGE_R,diagramName,"Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")")); + protocol.send(channel, new Reply(Reply.Name.TRANSLATE_EDGE_R, + diagramName, + "Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")", + source.getLocalSource()) + ); break; case BEND : - synchronized(diagram.getCollectionModel().getMonitor()){ - edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); - } + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); Point2D bendStart = null; if(cmd.getArgNum() == 5){ bendStart = new Point2D.Double((Double)cmd.getArgAt(3),(Double)cmd.getArgAt(4)); @@ -425,40 +634,119 @@ SwingUtilities.invokeLater(new CommandExecutor.Bend( edge, new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), - bendStart + bendStart, + source )); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.BEND_R,diagramName,"Bend at point: ("+(Double)cmd.getArgAt(1)+","+(Double)cmd.getArgAt(1)+")")); + protocol.send(channel, new Reply(Reply.Name.BEND_R,diagramName,"Bend at point: ("+(Double)cmd.getArgAt(1)+","+(Double)cmd.getArgAt(1)+")",cmd.getSource().getLocalSource())); 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)); + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge,source)); if(channel != localChannel) - protocol.send(channel, new Reply(Reply.Name.STOP_EDGE_MOVE_R,diagramName,"Undo straight bends")); + protocol.send(channel, new Reply(Reply.Name.STOP_EDGE_MOVE_R,diagramName,"Undo straight bends",source.getLocalSource())); 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)); + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(node,source)); if(channel != localChannel){ - protocol.send(channel, new Reply(Reply.Name.STOP_NODE_MOVE_R,diagramName,"Stop node move")); + protocol.send(channel, new Reply(Reply.Name.STOP_NODE_MOVE_R,diagramName,"Stop node move",source.getLocalSource())); } 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); + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel != localChannel && !sc.channel.equals(channel)){ + protocol.send(sc.channel, cmd); + } } } } - public Map<String, Queue<DiagramElement>> getLocalhostMap() { + private void handleAwarenessMessage(AwarenessMessage awMsg,SocketChannel channel, Diagram diagram) throws IOException { + if(diagram == null) + synchronized(diagrams){ + diagram = diagrams.get(awMsg.getDiagram()); + } + + if(awMsg.getName() == AwarenessMessage.Name.ERROR_A){ + Logger.getLogger(Server.class.getCanonicalName()).info((String)awMsg.getSource()); + return; + } + + /* for username aw msg checks whether the chosen name is not already used by another client. * + * If not changes the source from "newName", sent by the client, into "newName<SEPARATOR>oldName" * + * in order to broadcast it to the other client and make them replace the new name with the old one */ + String oldName = ""; + if(awMsg.getName() == AwarenessMessage.Name.USERNAME_A){ + String userName = (String)awMsg.getSource(); + UserNameSocketChannel userNameChannel = null; + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel.equals(channel)){ + userNameChannel = sc; + oldName = userNameChannel.userName; + + } + /* if another user already has the name then prevent from getting it */ + if(sc.userName.equals(userName)){ + /* user name already in use, send a reply and return */ + protocol.send(channel, new AwarenessMessage( + AwarenessMessage.Name.ERROR_A, + awMsg.getDiagram(), + ResourceBundle.getBundle(Server.class.getName()).getString("awareness.msg.user_already_exists") + )); + return; + } + } + userNameChannel.userName = userName; + /* set the source of the msg for the clients, which don't hold the channel-username association * + * and therefore need a message of the form "newName<SEPARATOR>oldName" in order to do the replacement */ + awMsg.setSource((String)awMsg.getSource()+AwarenessMessage.USERNAMES_SEPARATOR+oldName); + } + + /* update the local GUI to make the local user aware of the actions */ + DisplayFilter filter = DisplayFilter.getInstance(); + if(channel != localChannel && filter != null){ + if(awMsg.getName() == AwarenessMessage.Name.USERNAME_A){ + awarenessPanelEditor.replaceUserName(awMsg.getDiagram(), (String)awMsg.getSource()); + }else{ + DiagramEventActionSource processedSource = broadcastFilter.process((DiagramEventActionSource)awMsg.getSource()); // changes according to configuration; + if(filter.configurationHasChanged()){ + for(Diagram d : diagramChannelAllocations.keySet()) + awarenessPanelEditor.clearRecords(d.getName()); + } + + /* select and unselect are announced and written (temporary) on the panel, regardless START_A and STOP_A */ + if(processedSource.getCmd() == Command.Name.SELECT_NODE_FOR_EDGE_CREATION || processedSource.getCmd() == Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION){ + awarenessPanelEditor.addTimedRecord(awMsg.getDiagram(), filter.processForText(processedSource)); + NarratorFactory.getInstance().speakWholeText(filter.processForSpeech(processedSource), Narrator.SECOND_VOICE); + }else if(awMsg.getName() == AwarenessMessage.Name.START_A){ + awarenessPanelEditor.addRecord(awMsg.getDiagram(), filter.processForText(processedSource)); + /* announce the just received awareness message via the second voice */ + NarratorFactory.getInstance().speakWholeText(filter.processForSpeech(processedSource), Narrator.SECOND_VOICE); + }else{ // STOP_A + /* selection is a timedRecord, therefore no need to remove the record on STOP_A */ + if(processedSource.getCmd() != Command.Name.SELECT_NODE_FOR_EDGE_CREATION && processedSource.getCmd() != Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION) + awarenessPanelEditor.removeRecord(awMsg.getDiagram(), filter.processForText(processedSource)); + } + } + } + + /* broadcast the awareness message to all the clients but the local and * + * one which sent it, to inform them the action has started */ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel != localChannel && !sc.channel.equals(channel)) + protocol.send(sc.channel, awMsg); + } + } + + Map<String, Queue<DiagramElement>> getLocalhostMap() { return localhostDiagramElementQueue; } @@ -466,26 +754,60 @@ protocol.send(channel, new LockMessage( yes ? LockMessage.Name.YES_L : LockMessage.Name.NO_L, diagramName, - -1 + -1, + DiagramEventActionSource.NULL )); } - private Diagram diagram; - private Node node; - private Edge edge; - private DiagramElement de; - private DiagramModelTreeNode treeNode; + private DiagramEventActionSource removeEventActionSource(SocketChannel channel, long saveID, Diagram diagram){ + Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram); + if(userNames != null){ + for(UserNameSocketChannel userName : userNames){ + if(userName.channel.equals(channel)){ + for(DiagramEventActionSource s : userName.lockAwarenessSources){ + if(s.getSaveID() == saveID){ + userName.lockAwarenessSources.remove(s); + return s; + } + } + } + } + } + return null; + } + 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; + /* unique localChannel for all the digrams */ 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; + private Map<Diagram,Set<UserNameSocketChannel>> 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; + private BroadcastFilter broadcastFilter; + private AwarenessPanelEditor awarenessPanelEditor; + + /* this class holds for each socketChannel the username associated to it + * and the last received awareness message source,*/ + private static class UserNameSocketChannel { + UserNameSocketChannel(SocketChannel channel){ + this(channel,""); + } + + UserNameSocketChannel(SocketChannel channel, String userName){ + this.channel = channel; + this.userName = userName; + lockAwarenessSources = new LinkedList<DiagramEventActionSource>(); + } + + SocketChannel channel; + String userName; + List<DiagramEventActionSource> lockAwarenessSources; + } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/network/ServerLockManager.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ServerLockManager.java Wed Apr 25 17:09:09 2012 +0100 @@ -30,7 +30,7 @@ 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.DiagramTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.EdgeReferenceHolderMutableTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.EdgeReferenceMutableTreeNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeReferenceMutableTreeNode; @@ -74,7 +74,7 @@ } /* 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){ + private boolean lockExists(DiagramTreeNode 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; @@ -83,7 +83,7 @@ } /* 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){ + private boolean lockExists(DiagramTreeNode 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; @@ -93,7 +93,7 @@ /* 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){ + private boolean checkLockDependencies(DiagramTreeNode 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) @@ -155,7 +155,7 @@ /* 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)) + if(lockExists((DiagramTreeNode)enumeration.nextElement(),Lock.NOTES,Lock.BOOKMARK,locks,channel)) return false; } @@ -216,7 +216,8 @@ * holding a lock which clashes with this request) */ public static ServerLockManager singletonLockManager; - public boolean requestLock(DiagramModelTreeNode treeNode, Lock lock, SocketChannel channel,String diagramName){ + public boolean requestLock(DiagramTreeNode treeNode, Lock lock, SocketChannel channel,String diagramName){ +// System.out.println("lock before request:"+lockStatusDescription(diagramName)+"\n----"); List<LockEntry> locks = locksMap.get(diagramName); if(locks == null){ /* if no object in the diagram has ever been locked */ @@ -225,8 +226,8 @@ 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 */ + /* 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++){ @@ -254,8 +255,18 @@ } return false; } - locks.add(new LockEntry(treeNode,lock,channel)); - //System.out.println(lockStatusDescription(diagramName)); + + /* adds the lock only if it doesn't already exist */ + boolean add = true; + for(LockEntry l : locks){ + if(l.channel.equals(channel) && l.lock.equals(lock) && l.treeNode.equals(treeNode)){ + add = false; + break; + } + } + if(add) + locks.add(new LockEntry(treeNode,lock,channel)); +// System.out.println("lock after request:"+lockStatusDescription(diagramName)+"\n----"); return true; } /** @@ -264,14 +275,18 @@ * @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 + * + * @return true if a lock was really yielded as a result of the call */ - public void releaseLock(DiagramModelTreeNode treeNode, Lock lock, SocketChannel channel, String diagramName){ + public boolean releaseLock(DiagramTreeNode treeNode, Lock lock, SocketChannel channel, String diagramName){ List<LockEntry> locks = locksMap.get(diagramName); Iterator<LockEntry> iterator = locks.iterator(); + boolean lockReleased = false; while(iterator.hasNext()){ LockEntry entry = iterator.next(); if(entry.treeNode.equals(treeNode) && entry.lock == lock && entry.channel == channel){ iterator.remove(); + lockReleased = true; if(lock == Lock.DELETE && treeNode instanceof Node) continue; // we have to check for attached edges which must be unlocked too else @@ -287,7 +302,8 @@ } } } - //System.out.println(lockStatusDescription(diagramName)); +// System.out.println("lock release:"+lockStatusDescription(diagramName)+"\n----"); + return lockReleased; } /** @@ -357,7 +373,7 @@ private Map<String,List<LockEntry>> locksMap; private static class LockEntry { - public LockEntry(DiagramModelTreeNode treeNode,Lock lock,SocketChannel channel) { + public LockEntry(DiagramTreeNode treeNode,Lock lock,SocketChannel channel) { this.channel = channel; this.lock = lock; this.treeNode = treeNode; @@ -365,6 +381,6 @@ public SocketChannel channel; public Lock lock; - public DiagramModelTreeNode treeNode; + public DiagramTreeNode treeNode; } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/EdgeDrawSupport.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/EdgeDrawSupport.java Wed Apr 25 17:09:09 2012 +0100 @@ -49,7 +49,7 @@ * */ - public static void drawString(Graphics2D g2, //FIXME ArrowHead class should be in the same package + public static void drawString(Graphics2D g2, Point2D p, Point2D q, ArrowHead arrow, String s, boolean center){ if (s == null || s.length() == 0) return; label.setText("<html>" + s + "</html>");
--- a/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeEdge.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeEdge.java Wed Apr 25 17:09:09 2012 +0100 @@ -35,6 +35,7 @@ import org.w3c.dom.NodeList; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; import uk.ac.qmul.eecs.ccmi.gui.Edge; import uk.ac.qmul.eecs.ccmi.gui.GraphElement; import uk.ac.qmul.eecs.ccmi.gui.LineStyle; @@ -219,15 +220,15 @@ } @Override - public void setEndDescription(DiagramNode diagramNode, int index){ + public void setEndDescription(DiagramNode diagramNode, int index, Object source){ Node n = (Node)diagramNode; if(index == NO_END_DESCRIPTION_INDEX){ currentHeads.remove(n); - super.setEndDescription(n, index); + super.setEndDescription(n, index,source); }else{ ArrowHead h = heads[index]; currentHeads.put(n, h); - super.setEndDescription(n, index); + super.setEndDescription(n, index,source); } } @@ -277,26 +278,12 @@ break; } } - setEndDescription(nodesId.get(id),headDescriptionIndex); + setEndDescription(nodesId.get(id),headDescriptionIndex,DiagramEventSource.PERS); } } } @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() ); }
--- a/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardDialog.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardDialog.java Wed Apr 25 17:09:09 2012 +0100 @@ -67,7 +67,6 @@ button.addChangeListener(new javax.swing.event.ChangeListener(){ @Override public void stateChanged(ChangeEvent e) { - /* keep the button disabled until finished is not true */ ((JButton)e.getSource()).setEnabled(finished); } });
--- a/java/src/uk/ac/qmul/eecs/ccmi/sound/BeadsSound.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/BeadsSound.java Wed Apr 25 17:09:09 2012 +0100 @@ -159,16 +159,11 @@ } @Override - public void setPlayerListener(PlayerListener listener, SoundEvent type){ + public void setDefaultPlayerListener(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; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/sound/Sound.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/Sound.java Wed Apr 25 17:09:09 2012 +0100 @@ -49,11 +49,9 @@ public void stopLoop(SoundEvent evt); - public void setPlayerListener(PlayerListener listener, + public void setDefaultPlayerListener(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/speech/BeadsAudioPlayer.java Wed Apr 25 17:09:09 2012 +0100 @@ -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.speech; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + + +import net.beadsproject.beads.core.AudioContext; +import net.beadsproject.beads.core.Bead; +import net.beadsproject.beads.data.Sample; +import net.beadsproject.beads.ugens.Gain; +import net.beadsproject.beads.ugens.Panner; +import net.beadsproject.beads.ugens.SamplePlayer; + +import com.sun.speech.freetts.audio.AudioPlayer; + +public class BeadsAudioPlayer implements AudioPlayer { + public BeadsAudioPlayer(){ + format = new AudioFormat(8000f, 16, 1, true, true); + ac = new AudioContext(format); + volume = 1.0f; + monitor = new Object(); + } + + public BeadsAudioPlayer(float vol, float pan){ + this(); + volume = vol; + this.pan = pan; + } + + + @Override + public void begin(int size) { + buffer = new byte[size]; + bufferPosition = 0; + ac = new AudioContext(); + } + + @Override + public void cancel() { + + } + + @Override + public void close() { + ac.stop(); + } + + @Override + public boolean drain() { + synchronized(monitor){ + if(!finished) + try { + monitor.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + finished = false; + } + return false; + } + + @Override + public boolean end() { + ByteArrayInputStream stream = new ByteArrayInputStream(buffer); + AudioInputStream audioStream = new AudioInputStream(stream, format, bufferPosition/format.getFrameSize()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Sample sample = null; + try { + AudioSystem.write(audioStream, AudioFileFormat.Type.WAVE,out); + sample = new Sample(new ByteArrayInputStream(out.toByteArray())); + } catch (IOException e) { + e.printStackTrace(); + return false; + } catch (UnsupportedAudioFileException e) { + e.printStackTrace(); + return false; + } + + SamplePlayer player = new SamplePlayer(ac,sample); + player.setKillOnEnd(true); + Gain g = new Gain(ac,1,volume); + g.addInput(player); + final Panner panner = new Panner(ac,pan); + panner.addInput(g); + player.setKillListener(new Bead(){ + @Override + protected void messageReceived(Bead message){ + panner.kill(); + synchronized(monitor){ + finished = true; + monitor.notify(); + } + } + }); + + /* starts playing the sample */ + ac.out.addInput(panner); + ac.start(); + return true; + } + + @Override + public AudioFormat getAudioFormat() { + return format; + } + + @Override + public long getTime() { + return -1L; + } + + @Override + public float getVolume() { + return volume; + } + + @Override + public void pause() { + + } + + @Override + public void reset() { + + } + + @Override + public void resetTime() { + + } + + @Override + public void resume() { + + } + + @Override + public void setAudioFormat(AudioFormat format) { + this.format = format; + ac.setInputAudioFormat(format); + } + + @Override + public void setVolume(float vol) { + volume = vol; + } + + public void setPan(float pan){ + this.pan = pan; + } + + public float getPan(){ + return pan; + } + + @Override + public void showMetrics() { + + } + + @Override + public void startFirstSampleTimer() { + + } + + @Override + public boolean write(byte[] audioData) { + return write(audioData,0,audioData.length); + } + + @Override + public boolean write(byte[] audioData, int offset, int size) { + System.arraycopy(audioData, offset, buffer, bufferPosition, size); + bufferPosition += size; + return true; + } + + private byte[] buffer; + private int bufferPosition; + private AudioFormat format; + private float volume; + private float pan; + private Object monitor; + private boolean finished; + private AudioContext ac; + +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/speech/DummyNarrator.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/DummyNarrator.java Wed Apr 25 17:09:09 2012 +0100 @@ -19,7 +19,7 @@ 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. */ @@ -29,18 +29,27 @@ public void init() throws NarratorException {} @Override - public void setMuted(boolean muted) {} + public void setMuted(boolean muted, int voice) {} + + @Override + public boolean isMuted(int voice){return false;} @Override public void shutUp() {} @Override public void speak(String text) {} + + @Override + public void speak(String text, int voice){} @Override public void speakWholeText(String text){} @Override + public void speakWholeText(String text, int voice){} + + @Override public void dispose() {} @Override
--- a/java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.java Wed Apr 25 17:09:09 2012 +0100 @@ -28,7 +28,9 @@ public void init() throws NarratorException; - public void setMuted(boolean muted); + public void setMuted(boolean muted, int voice); + + public boolean isMuted(int voice); public void setRate(int rate); @@ -36,12 +38,19 @@ public void shutUp(); + public void speak(String text, int voice); + public void speak(String text); public void speakWholeText(String text); + + public void speakWholeText(String text,int voice); public void dispose(); int MIN_RATE = 0; int MAX_RATE = 20; + int FIRST_VOICE = 1; + int SECOND_VOICE = 2; + }
--- a/java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.properties Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.properties Wed Apr 25 17:09:09 2012 +0100 @@ -23,6 +23,8 @@ char.dash=dash char.underscore=underscore char.space=space +char.asterisk=asterisk +char.dollar=dollar error.no_speech=Could not create the speech synthesizer
--- a/java/src/uk/ac/qmul/eecs/ccmi/speech/NativeNarrator.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/NativeNarrator.java Wed Apr 25 17:09:09 2012 +0100 @@ -23,23 +23,26 @@ import java.util.ResourceBundle; import java.util.concurrent.LinkedBlockingQueue; -import uk.ac.qmul.eecs.ccmi.utils.NativeLibFileWriter; +import uk.ac.qmul.eecs.ccmi.utils.ResourceFileWriter; import uk.ac.qmul.eecs.ccmi.utils.OsDetector; import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; +import com.sun.speech.freetts.Voice; +import com.sun.speech.freetts.VoiceManager; /* * 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"); + ResourceFileWriter fileWriter = new ResourceFileWriter(url); + fileWriter.writeToDisk( + PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")), + "CCmIWinNarrator.dll"); String path = fileWriter.getFilePath(); if(path != null) try{ @@ -55,6 +58,15 @@ public NativeNarrator(){ resources = ResourceBundle.getBundle(Narrator.class.getName()); + VoiceManager voiceManager = VoiceManager.getInstance(); + secondaryVoice = voiceManager.getVoice(VOICE_NAME); + if(secondaryVoice == null) + System.out.println("Could not create voice for the second speaker"); + else{ + secondaryVoice.setAudioPlayer(new BeadsAudioPlayer(1.0f,1.0f)); + secondaryVoice.setRate(250f); + secondaryVoice.allocate(); + } } @Override @@ -62,7 +74,8 @@ if(nativeLibraryNotFound) throw new NarratorException(); - muted = false; + firstVoiceMuted = false; + secondVoiceMuted = false; queue = new LinkedBlockingQueue<QueueEntry>(); executor = new Executor(); boolean success = _init(); @@ -74,8 +87,19 @@ } @Override - public void setMuted(boolean muted) { - this.muted = muted; + public void setMuted(boolean muted, int voice) { + if(voice == SECOND_VOICE) + secondVoiceMuted = muted; + else + firstVoiceMuted = muted; + } + + @Override + public boolean isMuted(int voice){ + if(voice == SECOND_VOICE) + return secondVoiceMuted; + else + return firstVoiceMuted; } @Override @@ -98,8 +122,8 @@ } @Override - public void speak(String text) { - if(muted) + public void speak(String text, int voice) { + if(firstVoiceMuted || secondVoiceMuted && voice == Narrator.SECOND_VOICE) return; if(" ".equals(text)) text = resources.getString("char.space"); @@ -107,14 +131,29 @@ 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)); + queue.add(new QueueEntry(text,false,voice)); + } + + public void speak(String text){ + speak(text,Narrator.FIRST_VOICE); } @Override + public void speakWholeText(String text, int voice) { + if(firstVoiceMuted || secondVoiceMuted && voice == Narrator.SECOND_VOICE) + 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,true,voice)); + } + + @Override public void speakWholeText(String text) { - if(muted) - return; - queue.add(new QueueEntry(text,true)); + speakWholeText(text, Narrator.FIRST_VOICE); } @Override @@ -136,12 +175,15 @@ private native void _shutUp(); - private boolean muted; + private volatile boolean firstVoiceMuted; + private volatile boolean secondVoiceMuted; private int rate; private LinkedBlockingQueue<QueueEntry> queue; private Executor executor; private ResourceBundle resources; + private Voice secondaryVoice; private static String DEFAULT_RATE_VALUE = "13"; + private static final String VOICE_NAME = "kevin16"; private static boolean nativeLibraryNotFound; private class Executor extends Thread{ @@ -161,12 +203,16 @@ } if(!entry.speakToEnd && queue.peek() != null) continue;/* the user submitted another text to be spoken out and this can be overwritten */ - if(entry.speakToEnd){ + if(entry.speakToEnd && entry.voice == Narrator.FIRST_VOICE){ _speakWholeText(entry.text); - }else{ + }else if(entry.voice == Narrator.FIRST_VOICE){ _speak(entry.text); + }else if(secondaryVoice != null){ + secondaryVoice.speak(entry.text); } } + if(secondaryVoice != null) + secondaryVoice.deallocate(); _dispose(); } @@ -174,12 +220,14 @@ } private static class QueueEntry { - QueueEntry(String text, boolean speakToEnd){ + QueueEntry(String text, boolean speakToEnd, int voice){ this.text = text; this.speakToEnd = speakToEnd; + this.voice = voice; } String text; boolean speakToEnd; + int voice; } }
--- a/java/src/uk/ac/qmul/eecs/ccmi/speech/SpeechUtilities.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/SpeechUtilities.java Wed Apr 25 17:09:09 2012 +0100 @@ -42,14 +42,17 @@ import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JSpinner; +import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextField; +import javax.swing.JTree; 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; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; /** * A class providing static utilities methods concerning the text to speech synthesis. @@ -79,7 +82,12 @@ }else if(c instanceof JSpinner){ b.append(' ').append(resources.getString("component.spinner")); b.append(((JSpinner)c).getValue()); - }else{ + }else if(c instanceof JTabbedPane){ + Component comp = ((JTabbedPane)c).getSelectedComponent(); + if(comp == null) + return ""; + b.append(' ').append( comp.getName()); + }else if(!(c instanceof JTree)){ b.append(' ').append(c.getAccessibleContext().getAccessibleRole()); } return b.toString(); @@ -101,6 +109,7 @@ Component next = policy.getComponentAfter(container, KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); next.requestFocusInWindow(); NarratorFactory.getInstance().speak(SpeechUtilities.getComponentSpeech(next)); + InteractionLog.log("TABBED PANE","change focus ",next.getAccessibleContext().getAccessibleName()); } }); @@ -114,8 +123,17 @@ Component previous = policy.getComponentBefore(container, KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); previous.requestFocusInWindow(); NarratorFactory.getInstance().speak(SpeechUtilities.getComponentSpeech(previous)); + InteractionLog.log("TABBED PANE","change focus ",previous.getAccessibleContext().getAccessibleName()); } }); + + /* shut up the narrator upon pressing ctrl */ +// component.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"ctrldown"); +// component.getActionMap().put("ctrldown",new AbstractAction(){ +// public void actionPerformed(ActionEvent evt){ +// NarratorFactory.getInstance().shutUp(); +// } +// }); } private static void disableTraversalKey(Container container){ @@ -127,10 +145,19 @@ } } + public static KeyListener getSpeechKeyListener(boolean editableComponent, boolean secondVoice){ + if(!editableComponent) + return new SpeechKeyListener(false,secondVoice); + return speechKeyListener; + } + + /** + * Returns a {@code speechKeyListener} using first voice (default) + * @param editableComponent + * @return + */ public static KeyListener getSpeechKeyListener(boolean editableComponent){ - if(!editableComponent) - return new SpeechKeyListener(false); - return speechKeyListener; + return getSpeechKeyListener(editableComponent,false); } public static ItemListener getSpeechComboBoxItemListener(){ @@ -159,9 +186,11 @@ boolean isFirstLine; boolean isLastLine; boolean editableComponent; + int voice; - SpeechKeyListener(boolean editablecomponent){ + SpeechKeyListener(boolean editablecomponent, boolean useSecondVoice){ this.editableComponent = editablecomponent; + voice = useSecondVoice ? Narrator.SECOND_VOICE : Narrator.FIRST_VOICE; } @Override @@ -169,55 +198,61 @@ /* this will manage digit or letter characters */ if(!isTab && !evt.isControlDown() && editableComponent){ if(Character.isLetterOrDigit(evt.getKeyChar())){ - NarratorFactory.getInstance().speak(String.valueOf(evt.getKeyChar())); + NarratorFactory.getInstance().speak(String.valueOf(evt.getKeyChar()),voice); }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")); + NarratorFactory.getInstance().speak(resources.getString("char.new_line"),voice); break; case ' ' : - NarratorFactory.getInstance().speak(resources.getString("char.space")); + NarratorFactory.getInstance().speak(resources.getString("char.space"),voice); break; case '@' : - NarratorFactory.getInstance().speak(resources.getString("char.at")); + NarratorFactory.getInstance().speak(resources.getString("char.at"),voice); break; + case '*' : + NarratorFactory.getInstance().speak(resources.getString("char.asterisk"),voice); + break; + case '$' : + NarratorFactory.getInstance().speak(resources.getString("char.dollar"),voice); + break; case '.' : - NarratorFactory.getInstance().speak(resources.getString("char.dot")); + NarratorFactory.getInstance().speak(resources.getString("char.dot"),voice); break; case ',' : - NarratorFactory.getInstance().speak(resources.getString("char.comma")); + NarratorFactory.getInstance().speak(resources.getString("char.comma"),voice); break; case ';' : - NarratorFactory.getInstance().speak(resources.getString("char.semi_colon")); + NarratorFactory.getInstance().speak(resources.getString("char.semi_colon"),voice); break; case ':' : - NarratorFactory.getInstance().speak(resources.getString("char.colon")); + NarratorFactory.getInstance().speak(resources.getString("char.colon"),voice); break; case '<' : - NarratorFactory.getInstance().speak(resources.getString("char.lower_than")); + NarratorFactory.getInstance().speak(resources.getString("char.lower_than"),voice); break; case '>' : - NarratorFactory.getInstance().speak(resources.getString("char.greater_than")); + NarratorFactory.getInstance().speak(resources.getString("char.greater_than"),voice); break; case '#' : - NarratorFactory.getInstance().speak(resources.getString("char.sharp")); + NarratorFactory.getInstance().speak(resources.getString("char.sharp"),voice); break; case '~' : - NarratorFactory.getInstance().speak(resources.getString("char.tilde")); + NarratorFactory.getInstance().speak(resources.getString("char.tilde"),voice); break; case '+' : - NarratorFactory.getInstance().speak(resources.getString("char.plus")); + NarratorFactory.getInstance().speak(resources.getString("char.plus"),voice); break; case '-' : - NarratorFactory.getInstance().speak(resources.getString("char.dash")); + NarratorFactory.getInstance().speak(resources.getString("char.dash"),voice); break; case '_' : - NarratorFactory.getInstance().speak(resources.getString("char.underscore")); + NarratorFactory.getInstance().speak(resources.getString("char.underscore"),voice); break; case '/' : - NarratorFactory.getInstance().speak(resources.getString("char.slash")); + NarratorFactory.getInstance().speak(resources.getString("char.slash"),voice); break; } } @@ -261,10 +296,10 @@ switch(evt.getKeyCode()){ case KeyEvent.VK_BACK_SPACE: - NarratorFactory.getInstance().speak(resources.getString("char.back_space")); + NarratorFactory.getInstance().speak(resources.getString("char.back_space"),voice); break; case KeyEvent.VK_DELETE : - NarratorFactory.getInstance().speak(resources.getString("char.delete")); + NarratorFactory.getInstance().speak(resources.getString("char.delete"),voice); break; case KeyEvent.VK_LEFT : case KeyEvent.VK_RIGHT : @@ -280,7 +315,7 @@ return; } } - NarratorFactory.getInstance().speak(textComponent.getText(textComponent.getCaretPosition(),1)); + NarratorFactory.getInstance().speak(textComponent.getText(textComponent.getCaretPosition(),1),voice); } catch (BadLocationException e1) { e1.printStackTrace(); } @@ -311,7 +346,7 @@ if(end == begin)//in case it's an empty line end++; - NarratorFactory.getInstance().speak(text.substring(begin, end)); + NarratorFactory.getInstance().speak(text.substring(begin, end),voice); break; case KeyEvent.VK_DOWN : if(isLastLine){ //no new line we either have one line only or sit on the last one @@ -335,14 +370,14 @@ if(end == begin) // in case it's an empty line end++; - NarratorFactory.getInstance().speak(text.substring(begin, end)); + NarratorFactory.getInstance().speak(text.substring(begin, end),voice); break; } } } private static final ResourceBundle resources = ResourceBundle.getBundle(Narrator.class.getName()); - private static final SpeechKeyListener speechKeyListener = new SpeechKeyListener(true); + private static final SpeechKeyListener speechKeyListener = new SpeechKeyListener(true,false); private static final ItemListener comboBoxItemListener = new ItemListener(){ @Override public void itemStateChanged(ItemEvent evt) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/TreeSonifier.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,163 @@ +/* + 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.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; + +import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + */ +@SuppressWarnings("serial") +public class TreeSonifier { + + public void sonify(final JTree tree){ + /* select the root node as selected */ + tree.setSelectionRow(0); + /* Overwrite keystrokes up,down,left,right arrows and space, shift, ctrl */ + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0),"down"); + tree.getActionMap().put("down", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); + /* look if we've got a sibling node after (we are not at the bottom) */ + DefaultMutableTreeNode nextTreeNode = treeNode.getNextSibling(); + SoundEvent loop = null; + if(nextTreeNode == null){ + DefaultMutableTreeNode parent = (DefaultMutableTreeNode)treeNode.getParent(); + if(parent == null) /* root node, just stay there */ + nextTreeNode = treeNode; + else /* loop = go to first child of own parent */ + nextTreeNode = (DefaultMutableTreeNode)parent.getFirstChild(); + loop = SoundEvent.LIST_BOTTOM_REACHED; + } + tree.setSelectionPath(new TreePath(nextTreeNode.getPath())); + final String currentPathSpeech = currentPathSpeech(tree); + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + }}); + + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0),"up"); + tree.getActionMap().put("up", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); + DefaultMutableTreeNode previousTreeNode = treeNode.getPreviousSibling(); + SoundEvent loop = null; + if(previousTreeNode == null){ + DefaultMutableTreeNode parent = (DefaultMutableTreeNode)treeNode.getParent(); + if(parent == null) /* root node */ + previousTreeNode = treeNode; + else + previousTreeNode = (DefaultMutableTreeNode)parent.getLastChild(); + loop = SoundEvent.LIST_TOP_REACHED; + } + tree.setSelectionPath(new TreePath(previousTreeNode.getPath())); + final String currentPathSpeech = currentPathSpeech(tree); + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + }}); + + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0),"right"); + tree.getActionMap().put("right", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = tree.getSelectionPath(); + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)path.getLastPathComponent(); + if(treeNode.isLeaf()){ + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + else{ + tree.expandPath(path); + tree.setSelectionPath(new TreePath(((DefaultMutableTreeNode)treeNode.getFirstChild()).getPath())); + final String currentPathSpeech = currentPathSpeech(tree); + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_EXPAND,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + } + } + }); + + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0),"left"); + tree.getActionMap().put("left", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = tree.getSelectionPath(); + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)path.getLastPathComponent(); + DefaultMutableTreeNode parent = (DefaultMutableTreeNode)treeNode.getParent(); + if(parent == null){/* root node */ + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + else{ + TreePath newPath = new TreePath(((DefaultMutableTreeNode)parent).getPath()); + tree.setSelectionPath(newPath); + tree.collapsePath(newPath); + final String currentPathSpeech = currentPathSpeech(tree); + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_COLLAPSE,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + } + } + }); + + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,0),"space"); + tree.getActionMap().put("space",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + space(tree); + } + }); + /* make the tree ignore the page up and page down keys */ + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,0),"none"); + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,0),"none"); + } + + protected String currentPathSpeech(JTree tree) { + TreePath path = tree.getSelectionPath(); + DefaultMutableTreeNode selectedPathTreeNode = (DefaultMutableTreeNode)path.getLastPathComponent(); + return selectedPathTreeNode.toString(); + } + + protected void space(JTree tree){ + NarratorFactory.getInstance().speak(currentPathSpeech(tree)); + } + +}
--- a/java/src/uk/ac/qmul/eecs/ccmi/utils/GridBagUtilities.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/GridBagUtilities.java Wed Apr 25 17:09:09 2012 +0100 @@ -35,6 +35,13 @@ row = 0; } + /** + * Provides the {@code GridBagConstrains} for a label. The label is placed + * on the left + * @param pad the pad between the label and the left margin of the component containing + * it + * @return a {@code GridBagConstrains} object to pass to the {@code add} method of {@code JComponent} + */ public GridBagConstraints label(int pad){ GridBagConstraints c ; @@ -47,10 +54,20 @@ return c; } + /** + * Equivalent to {@link #label(int)} passing as argument the value previously + * set by {@link #setLabelPad(int)} or {@link #DEFAULT_LABEL_PAD} otherwise. + * @return + */ public GridBagConstraints label(){ return label(labelPad); } + /** + * Sets the value used by {@link #label()} as the pad between the label + * and the left margin of the component containing it + * @param labelPad the label pad + */ public void setLabelPad(int labelPad){ this.labelPad = labelPad; }
--- a/java/src/uk/ac/qmul/eecs/ccmi/utils/InteractionLog.java Mon Feb 06 12:54:06 2012 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/InteractionLog.java Wed Apr 25 17:09:09 2012 +0100 @@ -43,7 +43,7 @@ } /* also print the log on the console */ - java.util.logging.ConsoleHandler ch = new java.util.logging.ConsoleHandler(); + java.util.logging.ConsoleHandler ch = new java.util.logging.ConsoleHandler(); ch.setLevel(Level.ALL); ch.setFormatter(new CCmILogFormatter()); logger.addHandler(ch);
--- a/java/src/uk/ac/qmul/eecs/ccmi/utils/NativeLibFileWriter.java Mon Feb 06 12:54:06 2012 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,87 +0,0 @@ -/* - 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/ResourceFileWriter.java Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,99 @@ +/* + 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 ResourceFileWriter { + + /** + * 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 ResourceFileWriter(URL resource){ + this.resource = resource; + } + + public void serResource(URL resource){ + this.resource = resource;; + path = null; + } + + /** + * 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 dir,String fileName){ + if (resource == null) + return; + InputStream in = null; + FileOutputStream out = null; + File file = new File(dir,fileName); + if(file.exists()){ //if file already exists. no job needs to be done. + path = file.getAbsolutePath(); + return; + } + try{ + in = resource.openStream(); + out = new FileOutputStream(file); + int byteRead; + byte[] b = new byte[1024]; + while((byteRead = in.read(b)) > 0){ + out.write(b, 0, byteRead); + } + path = file.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){} + } + + } + + /** + * Returns the absolute path of the last written file. If the writing wasn't successfully + * or no writing took place yet, then {@code null} is returned. + * @return the path of the last written file or {@code null} + */ + public String getFilePath(){ + return path; + } + + private URL resource; + private String path; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/CollectionsManager.cpp Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,341 @@ +#include "CollectionsManager.h" + +void CollectionsManager::init(void){ + /**************** init jni variables **********************/ + + // --- classes --- + // get the java haptics class + hapticClass = env->GetObjectClass(*haptics); + if(hapticClass == NULL){ + stopExecution("Could not find the Haptics class"); + } + + bitsetClass = env->FindClass("Ljava/util/BitSet;"); + if(bitsetClass == NULL) + stopExecution("failed to find bitset class"); + + //get the node list class + listClass = env->FindClass("Ljava/util/ArrayList;"); + if(listClass == NULL) + stopExecution("failed to find list class"); + + // get the Node class to call the get method on the node list + hapticNodeClass = env->FindClass("Luk/ac/qmul/eecs/ccmi/haptics/Node;"); + if(hapticNodeClass == NULL){ + stopExecution("Could not find the Node class"); + } + + // get the Edge class to call the get method on the edge list + hapticEdgeClass = env->FindClass("Luk/ac/qmul/eecs/ccmi/haptics/Edge;"); + if(hapticEdgeClass == NULL){ + stopExecution("Could not find the Edge class"); + } + // --- methods --- + // "get" method id + getMethodId = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;"); + if(getMethodId == NULL) + stopExecution("Could not retrieve the get method id"); + + //size method id + sizeMethodId = env->GetMethodID(listClass, "size", "()I"); + if(sizeMethodId == NULL) + stopExecution("failed to get size method id"); + + //get method of the BitSet class + getBitMethodId = env->GetMethodID(bitsetClass, "get","(I)Z"); + if(getBitMethodId == NULL) + stopExecution("failed to get the get method id of the BitSet class"); + + getNodeFromIDMethodId = env->GetMethodID(hapticClass,"getNodeFromID","(I)Luk/ac/qmul/eecs/ccmi/haptics/Node;"); + if(getNodeFromIDMethodId == NULL){ + stopExecution("failed to get getNodeFromID of the Haptics class"); + } + + getEdgeFromIDMethodId = env->GetMethodID(hapticClass,"getEdgeFromID","(I)Luk/ac/qmul/eecs/ccmi/haptics/Edge;"); + if(getEdgeFromIDMethodId == NULL){ + stopExecution("failed to get getEdgeFromID of the Haptics class"); + } + + // -- field id's -- + //retrieve edgeList field id + edgeListfieldId = env->GetFieldID(hapticClass,"currentEdges", "Ljava/util/ArrayList;"); + if(edgeListfieldId == NULL){ + stopExecution("failed to find the edge list field id"); + } + + //retrieve nodeList field id + nodeListfieldId = env->GetFieldID(hapticClass,"currentNodes", "Ljava/util/ArrayList;"); + if(nodeListfieldId == NULL){ + stopExecution("failed to find the node list field id"); + } + + // Node fields id's + xFieldId = env->GetFieldID(hapticNodeClass,"x","D"); + if(xFieldId == NULL) + stopExecution("Could not find the x field ID"); + + + yFieldId = env->GetFieldID(hapticNodeClass,"y","D"); + if(yFieldId == NULL) + stopExecution("Could not find the y field ID"); + + nodeHapticIdFieldId = env->GetFieldID(hapticNodeClass, "hapticId", "I"); + if(nodeHapticIdFieldId == NULL) + stopExecution("Could not find the node hapticId field ID"); + + nodeDiagramIdFieldId = env->GetFieldID(hapticNodeClass, "diagramId", "I"); + if(nodeDiagramIdFieldId == NULL) + stopExecution("Could not find the node diagramId field ID"); + + // Edge field id's + // edge.hapticId + edgeHapticIdFieldId = env->GetFieldID(hapticEdgeClass, "hapticId", "I"); + if(edgeHapticIdFieldId == NULL) + stopExecution("Could not find the edge hapticId field ID"); + + edgeDiagramIdFieldId = env->GetFieldID(hapticEdgeClass, "diagramId", "I"); + if(edgeDiagramIdFieldId == NULL) + stopExecution("Could not find the edge diagramId field ID"); + + // stipplePattern + stipplePatternfieldId = env->GetFieldID(hapticEdgeClass, "stipplePattern", "I"); + if(stipplePatternfieldId == NULL){ + stopExecution("Could not find the stipplePattern field ID"); + } + + edgeSizefieldId = env->GetFieldID(hapticEdgeClass, "size", "I"); + if(edgeSizefieldId == NULL) + stopExecution("Could not find edge size field ID"); + + edgeXsFieldId = env->GetFieldID(hapticEdgeClass,"xs", "[D"); + if(edgeXsFieldId == NULL) + stopExecution("Could not find edge xs field ID"); + + edgeYsFieldId = env->GetFieldID(hapticEdgeClass,"ys", "[D"); + if(edgeYsFieldId == NULL) + stopExecution("Could not find edge ys field ID"); + + edgeAdjMatrixFieldId = env->GetFieldID(hapticEdgeClass,"adjMatrix", "[Ljava/util/BitSet;"); + if(edgeAdjMatrixFieldId == NULL) + stopExecution("Could not find edge adjMatrix field ID"); + + attractPointXFieldId = env->GetFieldID(hapticEdgeClass, "attractPointX", "D"); + if(attractPointXFieldId == NULL) + stopExecution("Could not find the edge attractPointX field ID"); + + attractPointYFieldId = env->GetFieldID(hapticEdgeClass, "attractPointY", "D"); + if(attractPointYFieldId == NULL) + stopExecution("Could not find the edge attractPointY field ID"); + + edgeNodeStartFieldId = env->GetFieldID(hapticEdgeClass, "nodeStart", "I"); + if(edgeNodeStartFieldId == NULL) + stopExecution("Could not find the edge nodeStart field ID"); +} + +const jint CollectionsManager::getNodesNum() { + /* to read the node list field into the nodeList variable each time we get the size + * is needed as, when the tab is switched the nodeList variables still points to the + * previuos tab's node list. We do it only here and not in getNodeData because + * getNodesData follows this call before releasing the monitor, therefore + * data integrity is granted. + */ + env->DeleteLocalRef(nodeList); + nodeList = env->GetObjectField(*haptics, nodeListfieldId); + if(nodeList == NULL){ + stopExecution("could not get the node list field of Haptic Class"); + } + + jint size = env->CallIntMethod( nodeList, sizeMethodId); + checkExceptions(env,"Could not call ArrayList<Node>.size()"); + return size; +} + +const jint CollectionsManager::getEdgesNum(){ + env->DeleteLocalRef(edgeList); + edgeList = env->GetObjectField(*haptics, edgeListfieldId); + if(edgeList == NULL){ + stopExecution("could not get the edge list field of Haptic Class"); + } + + jint size = env->CallIntMethod( edgeList, sizeMethodId); + checkExceptions(env, "Could not call ArrayList<Edge>.size()"); + return size; +} + +CollectionsManager::NodeData & CollectionsManager::getNodeData(const int i){ + // get the i-th node + jobject currentNode = env->CallObjectMethod(nodeList,getMethodId,i); + checkExceptions(env,"Could not call ArrayList<Node>.size()"); + fillupNodeData(nd,currentNode); + env->DeleteLocalRef(currentNode); + return nd; +} + +CollectionsManager::EdgeData & CollectionsManager::getEdgeData(const int i){ + /* first we look for the i-th edge in the Haptics java class. Once we get it, + * we need all the coordinates of the node this edge is connecting, so that we + * can draw it. The edge mantains a list of references to such nodes. + */ + + // get the i-th edge + jobject currentEdge = env->CallObjectMethod(edgeList,getMethodId,i); + checkExceptions(env, "Could not call ArrayList<Edge>.get(int) in the haptics edges"); + if(currentEdge == NULL) + stopExecution("Could not find get current Edge"); + jint size = env->GetIntField(currentEdge, edgeSizefieldId); + ed.setSize(size); + fillupEdgeData(ed,currentEdge); + env->DeleteLocalRef(currentEdge); + return ed; +} + +bool CollectionsManager::isNode(const jint id) const{ + if(env->MonitorEnter(*haptics) != JNI_OK){ + throw(NULL); + } + jobject currentNode = env->CallObjectMethod(*haptics,getNodeFromIDMethodId,id); + checkExceptions(env, "Could not call Haptics.getNodeFromIDMethodId in CollectionsManager::isNode"); + bool retVal = (currentNode != NULL); + env->DeleteLocalRef(currentNode); + if(env->MonitorExit(*haptics) != JNI_OK){ + throw(NULL); + } + return retVal; +} + +CollectionsManager::NodeData & CollectionsManager::getNodeDataFromID(const jint id){ + if(env->MonitorEnter(*haptics) != JNI_OK){ + throw(NULL); + } + jobject currentNode = env->CallObjectMethod(*haptics,getNodeFromIDMethodId,id); + checkExceptions(env, "Could not call Haptics.getNodeFromIDMethodId in CollectionsManager::getNodeDataFromID"); + if(currentNode == NULL){ + env->DeleteLocalRef(currentNode); + throw(NULL); + } + fillupNodeData(nd,currentNode); + env->DeleteLocalRef(currentNode); + if(env->MonitorExit(*haptics) != JNI_OK){ + throw(NULL); + } + return nd; +} + +bool CollectionsManager::isEdge(const jint id) const{ + if(env->MonitorEnter(*haptics) != JNI_OK){ + throw(NULL); + } + jobject currentEdge = env->CallObjectMethod(*haptics,getEdgeFromIDMethodId,id); + checkExceptions(env, "Could not call Haptics.getEdgeFromIDMethodId in CollectionsManager::isEdge"); + bool retVal = (currentEdge != NULL); + env->DeleteLocalRef(currentEdge); + if(env->MonitorExit(*haptics) != JNI_OK){ + throw(NULL); + } + return retVal; +} + +CollectionsManager::EdgeData & CollectionsManager::getEdgeDataFromID(const jint id){ + if(env->MonitorEnter(*haptics) != JNI_OK){ + throw(NULL); + } + jobject currentEdge = env->CallObjectMethod(*haptics,getEdgeFromIDMethodId,id); + checkExceptions(env, "Could not call Haptics.getEdgeFromIDMethodId in CollectionsManager::getEdgeDataFromID"); + if(currentEdge == NULL){ + env->DeleteLocalRef(currentEdge); + throw(NULL); + } + + jint size = env->GetIntField(currentEdge, edgeSizefieldId); + ed.setSize(size); + fillupEdgeData(ed,currentEdge); + env->DeleteLocalRef(currentEdge); + if(env->MonitorExit(*haptics) != JNI_OK){ + throw(NULL); + } + return ed; +} + +void CollectionsManager::fillupNodeData(NodeData & nd, jobject & currentNode){ + //reads the fields of the current node + nd.x = env->GetDoubleField(currentNode,xFieldId); + nd.y = env->GetDoubleField(currentNode,yFieldId); + hduVector3Dd glCoordinatePosition; + // takes coordinates from the screen. Needs to convert the y axis as in openGL (0,0) = bottom left corner + fromScreen(hduVector3Dd(nd.x, screenHeight - nd.y, 0),glCoordinatePosition); + nd.x = glCoordinatePosition[0]; + nd.y = glCoordinatePosition[1]; + nd.hapticId = env->GetIntField(currentNode, nodeHapticIdFieldId); + nd.diagramId = env->GetIntField(currentNode, nodeDiagramIdFieldId); +} + +void CollectionsManager::fillupEdgeData(EdgeData & ed, jobject & currentEdge){ + /* get the array of x coordinates */ + jobject xsAsObj = env->GetObjectField(currentEdge,edgeXsFieldId); + if(xsAsObj == NULL) + stopExecution("Cannot get the xs field"); + jdoubleArray *jxs = reinterpret_cast<jdoubleArray*>(&xsAsObj); + double * xs = env->GetDoubleArrayElements(*jxs, NULL); + if(xs == NULL) + stopExecution("Cannot get the xs field array of double"); + + /* get the array of y coordinates */ + jobject ysAsObj = env->GetObjectField(currentEdge,edgeYsFieldId); + if(ysAsObj == NULL) + stopExecution("Cannot get the xs field"); + + jdoubleArray *jys = reinterpret_cast<jdoubleArray*>(&ysAsObj); + double * ys = env->GetDoubleArrayElements(*jys, NULL); + if(ys == NULL) + stopExecution("Cannot get the ys field array of double"); + // copy the data into the edgeData object + for(unsigned int i=0; i<ed.getSize(); i++){ + ed.x[i] = xs[i]; + ed.y[i] = ys[i]; + // takes coordinates from the screen (needs to convert the y axis as in openGL 0 = bottom left corner + hduVector3Dd glCoordinatePosition; + fromScreen(hduVector3Dd(ed.x[i], screenHeight - ed.y[i], 0),glCoordinatePosition); + ed.x[i] = glCoordinatePosition[0]; + ed.y[i] = glCoordinatePosition[1]; + } + env->ReleaseDoubleArrayElements(*jxs, xs, 0); + env->ReleaseDoubleArrayElements(*jys, ys, 0); + env->DeleteLocalRef(xsAsObj); + env->DeleteLocalRef(ysAsObj); + jobject adjMatrixAsObj = env->GetObjectField(currentEdge, edgeAdjMatrixFieldId); + if(adjMatrixAsObj == NULL) + stopExecution("Cannot get the adjMatrix field"); + jobjectArray *jadjMatrix = reinterpret_cast<jobjectArray*>(&adjMatrixAsObj); + + for(unsigned int i=0; i<ed.getSize(); i++){ + jobject adjMatrixBitSet = env->GetObjectArrayElement(*jadjMatrix,i); + if(adjMatrixBitSet == NULL) + stopExecution("Cannot get the adjMatrix field array element"); + for(unsigned int j=0;j<ed.getSize(); j++){ + jboolean b = env->CallBooleanMethod(adjMatrixBitSet,getBitMethodId,j); + checkExceptions(env,"Could not call BitSet.get()"); + if(b == JNI_TRUE) + ed.adjMatrix[i][j] = true; + else + ed.adjMatrix[i][j] = false; + } + env->DeleteLocalRef(adjMatrixBitSet); + } + env->DeleteLocalRef(adjMatrixAsObj); + + // set the haptic id used by the haptic device + ed.hapticId = env->GetIntField(currentEdge, edgeHapticIdFieldId); + // set the diagram id used in the java thread + ed.diagramId = env->GetIntField(currentEdge, edgeDiagramIdFieldId); + // set the stipple pattern + ed.stipplePattern = env->GetIntField(currentEdge, stipplePatternfieldId); + // set the attract point + ed.attractPoint[0] = env->GetDoubleField(currentEdge, attractPointXFieldId); + ed.attractPoint[1] = env->GetDoubleField(currentEdge, attractPointYFieldId); + hduVector3Dd glCoordinatePosition; + fromScreen(hduVector3Dd(ed.attractPoint[0], screenHeight - ed.attractPoint[1], 0),glCoordinatePosition); + ed.attractPoint[0] = glCoordinatePosition[0]; + ed.attractPoint[1] = glCoordinatePosition[1]; + //set the index from which the nodes start in the adjMatrix + ed.nodeStart = env->GetIntField(currentEdge, edgeNodeStartFieldId); +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/CollectionsManager.h Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,140 @@ +#pragma once + +#include <jni.h> +#include "stdafx.h" +#include "utils.h" + + +/* this class uses the java haptic object lists to provide nodes * + * and edges to draw to the graphic and haptic managers */ +class CollectionsManager +{ +public: + struct NodeData { + jdouble x; + jdouble y; + jint hapticId; + jint diagramId; + + NodeData(){} + private : + NodeData(const NodeData& n){/* avoid mistakenly copy construction calls */} + }; + + struct EdgeData{ + jdouble *x; + jdouble *y; + bool **adjMatrix; + jint hapticId; + jint diagramId; + jint stipplePattern; + jsize nodeStart; + jdouble attractPoint[2]; + void setSize(unsigned int s){ + size = s; + if(s > previousSize){ + /* delete the old memory */ + delete [] x; + delete [] y; + for(unsigned int i=0; i<previousSize; i++) + delete[] adjMatrix[i]; + delete [] adjMatrix; + /* allocates a bigger one */ + try{ + x = new jdouble[size]; + y = new jdouble[size]; + adjMatrix = new bool* [size]; + for(unsigned int i=0; i<size; i++){ + adjMatrix[i] = new bool[size]; + for(unsigned int j=0; j<size; j++) + adjMatrix[i][j] = false; + } + }catch(std::bad_alloc){ + stopExecution("Could not allocate memory for the program.\nAborting..."); + } + previousSize = size; + } + } + + unsigned int getSize() const { + return size; + } + + EdgeData(unsigned int s) : size(s), previousSize(s){ + x = new jdouble[size]; + y = new jdouble[size]; + adjMatrix = new bool* [size]; + for(unsigned int i=0; i<size; i++){ + adjMatrix[i] = new bool[size]; + for(unsigned int j=0; j<size; j++) + adjMatrix[i][j] = false; + } + } + ~EdgeData(){ + delete [] x; + delete [] y; + for(unsigned int i=0; i<size; i++) + delete[] adjMatrix[i]; + delete [] adjMatrix; + } + private : + unsigned int size; + unsigned int previousSize; + EdgeData(const EdgeData& e){ /* avoid mistakenly copy construction */} + + }; +private: + JNIEnv *env; + jobject *haptics; + + jclass hapticClass; + jclass hapticNodeClass; + jclass hapticEdgeClass; + jclass listClass; + jclass bitsetClass; + jobject nodeList; + jobject edgeList; + jmethodID getMethodId; + jmethodID sizeMethodId; + jmethodID getBitMethodId; + jmethodID getNodeFromIDMethodId; + jmethodID getEdgeFromIDMethodId; + jfieldID xFieldId; + jfieldID yFieldId; + jfieldID nodeHapticIdFieldId; + jfieldID edgeHapticIdFieldId; + jfieldID nodeDiagramIdFieldId; + jfieldID edgeDiagramIdFieldId; + jfieldID nodeListfieldId; + jfieldID stipplePatternfieldId; + jfieldID edgeListfieldId; + jfieldID edgeSizefieldId; + jfieldID edgeXsFieldId; + jfieldID edgeYsFieldId; + jfieldID edgeAdjMatrixFieldId; + jfieldID attractPointXFieldId; + jfieldID attractPointYFieldId; + jfieldID edgeNodeStartFieldId; + + int screenHeight; + NodeData nd; + EdgeData ed; + + void fillupNodeData(struct NodeData & nd, jobject & currentNode); + void fillupEdgeData(struct EdgeData & ed, jobject & currentEdge); +public: + CollectionsManager(JNIEnv *environment, jobject *obj) : env(environment), haptics(obj), ed(2){} + virtual ~CollectionsManager(void){} + const jint getNodesNum(); + const jint getEdgesNum(); + struct NodeData & getNodeData(const int i); + struct EdgeData & getEdgeData(const int i); + struct NodeData & getNodeDataFromID(const jint id) throw(int); + struct EdgeData & getEdgeDataFromID(const jint id) throw(int); + bool isEdge(const jint id) const throw(int); + bool isNode(const jint id) const throw(int); + void init(void); + int getScreenHeight() const { return screenHeight;} + void setScreenHeight(const int sh) { screenHeight = sh; } + +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/GraphicManager.cpp Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,143 @@ +#include "GraphicManager.h" + +void GraphicManager::init(void){ + + // Enable depth buffering for hidden surface removal. + glDepthFunc(GL_LEQUAL); + glEnable(GL_DEPTH_TEST); + + // Cull back faces. + glCullFace(GL_BACK); + glEnable(GL_CULL_FACE); + + // Setup other misc features. + glEnable(GL_LIGHTING); + glEnable(GL_NORMALIZE); + glShadeModel(GL_SMOOTH); + + // Setup lighting model. + glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_FALSE); + glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE); + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, light_model_ambient); + glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse); + glLightfv(GL_LIGHT0, GL_POSITION, light0_direction); + glEnable(GL_LIGHT0); + + +} + +void GraphicManager::draw(void){ + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // Draw 3D cursor at haptic device position. + drawCursor(); + + glPushAttrib(GL_CURRENT_BIT | GL_ENABLE_BIT); + glEnable(GL_COLOR_MATERIAL); + + // Draw each of the nodes as lit sphere. + int numNodes = collectionsManager->getNodesNum(); + GLdouble dPointSize; + + for ( int i = 0; i < numNodes; i++){ + dPointSize = nodeSize; + glColor3dv(nodeColor); + + glPushMatrix(); + CollectionsManager::NodeData & nd = collectionsManager->getNodeData(i); + glTranslated(nd.x,nd.y, 0); + double dPointScale = dPointSize * gWorldScale; + glScaled(dPointScale, dPointScale, dPointScale); + /* draw shpere */ + glutSolidSphere(0.5, 10, 10); + glPopMatrix(); + } + + //draw edges + glColor3f(1.0, 0.0, 0.0); + glLineWidth(2.0); + glPushAttrib(GL_ENABLE_BIT); + glDisable(GL_LIGHTING); + glEnable(GL_COLOR_MATERIAL); + + int numEdges = collectionsManager->getEdgesNum(); + for(int i = 0; i < numEdges; i++){ + CollectionsManager::EdgeData & ed = collectionsManager->getEdgeData(i); + glPushAttrib(GL_ENABLE_BIT); + glLineStipple(1, ed.stipplePattern); + glEnable(GL_LINE_STIPPLE); + glBegin(GL_LINES); + for(unsigned int j = 0; j < ed.getSize(); j++){ + for(unsigned int k = j; k < ed.getSize(); k++){ + if(ed.adjMatrix[j][k]){ + glVertex3d(ed.x[j],ed.y[j],0); + glVertex3d(ed.x[k],ed.y[k],0); + } + } + } + glEnd(); + glPopAttrib(); + } + glPopAttrib(); + glPopMatrix(); + +} + +void GraphicManager::drawCursor(){ + HLdouble proxyxform[16]; + + GLUquadricObj *qobj = 0; + + glPushAttrib(GL_CURRENT_BIT | GL_ENABLE_BIT | GL_LIGHTING_BIT); + glPushMatrix(); + if (!gCursorDisplayList){ + gCursorDisplayList = glGenLists(1); + glNewList(gCursorDisplayList, GL_COMPILE); + qobj = gluNewQuadric(); + + gluCylinder(qobj, 0.0, kCursorRadius, kCursorHeight, + kCursorTess, kCursorTess); + glTranslated(0.0, 0.0, kCursorHeight); + gluCylinder(qobj, kCursorRadius, 0.0, kCursorHeight / 5.0, + kCursorTess, kCursorTess); + + gluDeleteQuadric(qobj); + glEndList(); + } + + // Get the proxy transform in world coordinates. + hlGetDoublev(HL_PROXY_TRANSFORM, proxyxform); + glMultMatrixd(proxyxform); + + // Apply the local cursor scale factor. + glScaled(gCursorScale, gCursorScale, gCursorScale); + + glEnable(GL_COLOR_MATERIAL); + + if(hapticManager->isDraggingNode()){ + glColor3dv(nodeColor); + glutSolidSphere(0.25, 10, 10); + }else if(hapticManager->isDraggingEdge()){ + glColor3dv(edgeColor); + glutSolidSphere(0.12,10,10); + } + glColor3f(0.0, 0.5, 1.0); + glCallList(gCursorDisplayList); + + glPopMatrix(); + glPopAttrib(); +} + +const double GraphicManager::kCursorRadius = 0.5; +const double GraphicManager::kCursorHeight = 1.5; +const int GraphicManager::kCursorTess = 15; + +const GLfloat GraphicManager::light_model_ambient[] = {0.3f, 0.3f, 0.3f, 1.0f}; +const GLfloat GraphicManager::light0_diffuse[] = {0.9f, 0.9f, 0.9f, 0.9f}; +const GLfloat GraphicManager::light0_direction[] = {0.0f, -0.4f, 1.0f, 0.0f}; + + +const double GraphicManager::nodeSize = 10; + +const hduVector3Dd GraphicManager::nodeColor(1.0, 1.0, 1.0); +const hduVector3Dd GraphicManager::edgeColor(1.0,0.0,0.0); +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/GraphicManager.h Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,39 @@ +#pragma once + +#include "stdafx.h" +#include "CollectionsManager.h" +#include "HapticManager.h" + +/* The graphic manager draws the diagram openGL scene graphically. * + * In order to get all the data about the diagram it's constructed * + * passing a reference to the CollectionManager */ +class GraphicManager{ + static const double kCursorRadius; + static const double kCursorHeight; + static const int kCursorTess; + static const GLfloat light_model_ambient[]; + static const GLfloat light0_diffuse[]; + static const GLfloat light0_direction[]; + + static const double nodeSize; + + static const hduVector3Dd nodeColor;//(1.0, 1.0, 1.0); + static const hduVector3Dd edgeColor; + + CollectionsManager * collectionsManager; + HapticManager * hapticManager; + + void drawCursor(void); +public: + double gCursorScale; + double gWorldScale; + GLuint gCursorDisplayList; + GraphicManager(CollectionsManager * cManager, HapticManager * hManager) : collectionsManager(cManager), hapticManager(hManager) { + gCursorDisplayList = 0; + gWorldScale = 1; + }; + ~GraphicManager(void){}; + void init(void); + void draw(void); + +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/HapticException.cpp Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,11 @@ +#include "HapticException.h" + +HapticException::HapticException(void){ +} + +HapticException::~HapticException(void){ +} + +const char* HapticException::what() const throw(){ + return "Could not initialize Haptic device"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/HapticException.h Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,14 @@ +#pragma once +#include <exception> + +using namespace std; + +/* This exception is thrown by the HapticManager if the initialization of the Haptic + * doesn't succeed. + */ +class HapticException : public exception { +public: + HapticException(void); + virtual ~HapticException(void); + virtual const char* what() const throw(); +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/HapticManager.cpp Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,672 @@ +#include "HapticManager.h" + +#define NO_ERROR_SIMUL + + +void checkExceptions(void); +void stopExecution(char* msg); + +bool movedNode = false; + + +void HLCALLBACK HapticManager::hlButton1CB(HLenum event, HLuint object, + HLenum thread, HLcache *cache, + void *userdata) +{ + HapticManager *pThis = static_cast<HapticManager*>(userdata); + if(object == pThis->wall1Id||object == pThis->wall2Id) // no event is associated to the wall + return; + if(event != HL_EVENT_1BUTTONDOWN) + return; + + /* check if it was a double click or not */ + + clock_t time = clock(); + if(pThis->clickStart == 0) // first click + pThis->clickStart = time; + else if(time < (pThis->clickStart + CLICK_INTERVAL)) + pThis->doubleClick = true; + +} + +void HLCALLBACK HapticManager::hlButton2CB(HLenum event, HLuint object, + HLenum thread, HLcache *cache, + void *userdata){ + HapticManager *pThis = static_cast<HapticManager*>(userdata); + if(event == HL_EVENT_2BUTTONDOWN){ + pThis->stickyMode = !pThis->stickyMode; + pThis->executeCommand('g', pThis->stickyMode ? 1 : 0,0,0,0,0); + /* we change mode to stickyMode, thus dragging becomes not active */ + if(pThis->stickyMode && pThis->springStatus == DRAGGING){ + pThis->springStatus = STOP_DRAGGING; + pThis->secondClick = false; + } +#ifdef VIBRATION + // vibrate + if(pThis->vibrationStatus == NO_VIBRATION){ + pThis->vibrationStatus = START_VIBRATION; + } +#endif + } +} + +void HLCALLBACK HapticManager::hlTouchCB(HLenum event, HLuint object, HLenum thread, + HLcache *cache, void *userdata) +{ + HapticManager *pThis = static_cast<HapticManager*>(userdata); + if(object == pThis->wall1Id||object == pThis->wall2Id) // no event is associated to the wall + return; + if(pThis->lastMovedElement == object){ + pThis->stillTouchingLastMovedElement = true; + } + + try{ + // check if the touched object is an edge + if(pThis->collectionsManager->isEdge(object)){ + CollectionsManager::EdgeData & ed = pThis->collectionsManager->getEdgeDataFromID(object); + pThis->lastTouchedEdgeID = object; + pThis->lastTouchedEdgeIsUntouched = false; + pThis->lastTouchedEdgeStipplePattern = ed.stipplePattern; + // check if the proxy is still touching the edge's node, in that case only play the sound + HLboolean isTouching = HL_FALSE; + if(pThis->lastTouchedNodeID != HL_OBJECT_ANY){ + hlGetShapeBooleanv(pThis->lastTouchedNodeID,HL_PROXY_IS_TOUCHING,&isTouching); + } + /* play the sound and speech associated to the edge */ + if((isTouching == HL_TRUE||pThis->stillTouchingLastMovedElement) && (object != pThis->attractToHapticId)){ + if(!pThis->stickyMode) + pThis->executeCommand('p',ed.diagramId,0,0,0,0); // just plays the sound + }else{ + pThis->executeCommand('s',ed.diagramId,0,0,0,0); + pThis->lastHighlightElementID = object; + pThis->executeCommand('t',ed.diagramId,0,0,0,0); // speaks the edge's name + } + }else{ // it's a node + pThis->lastTouchedNodeID = object; + CollectionsManager::NodeData & nd = pThis->collectionsManager->getNodeDataFromID(object); + pThis->lastTouchedNodePosition[0] = nd.x; + pThis->lastTouchedNodePosition[1] = nd.y; + pThis->lastTouchedNodePosition[2] = 0; + pThis->frictionDisabled = true; + pThis->executeCommand('s',nd.diagramId,0,0,0,0); + pThis->lastHighlightElementID = object; + pThis->executeCommand('t',nd.diagramId,0,0,0,0); + } + }catch(int){ + stopExecution("Touch Call Back: Could not retrieve element from ID"); + } +} + +void HLCALLBACK HapticManager::hlUntouchCB(HLenum event, HLuint object, HLenum thread, + HLcache *cache, void *userdata) +{ + HapticManager *pThis = static_cast<HapticManager*>(userdata); + if(object == pThis->wall1Id||object == pThis->wall2Id) // no event is associated to the wall + return; + + if(object == pThis->lastMovedElement){ + pThis->lastMovedElement = HL_OBJECT_ANY; + pThis->stillTouchingLastMovedElement = false; + } + if(pThis->lastHighlightElementID == object){ + pThis->lastHighlightElementID = HL_OBJECT_ANY; + pThis->executeCommand('u',0,0,0,0,0); + } + if(pThis->lastTouchedEdgeID == object){ + pThis->lastTouchedEdgeIsUntouched = true; + } +} + + +void HLCALLBACK HapticManager::hlMotionCB(HLenum event, HLuint object, HLenum thread, + HLcache *cache, void *userdata) +{ + HapticManager *pThis = static_cast<HapticManager*>(userdata); + + /* plays the sound to acknowledge the user they're dragging an element around */ + static int onceinawhile = 0; + if(pThis->springStatus == DRAGGING){ + onceinawhile = (onceinawhile+1)%5; + if(!onceinawhile) + pThis->executeCommand('g',3,0,0,0,0); + } + + /* the following code is to play element speech when taking a path through an edge starting from a connected node */ + + /* friction is disabled when we land on a node. If proxy ain't on a node, then just return */ + /* else it would play every time we move along the edge even if not detouching from a node */ + if(!pThis->frictionDisabled){ + return; + } + + /* calculate the distance between the node, the stylus was on, and the current proxy position after moving out from it*/ + HLdouble proxyPosition[3]; + hlGetDoublev(HL_DEVICE_POSITION,proxyPosition); + proxyPosition[0] -= pThis->lastTouchedNodePosition[0]; + proxyPosition[1] -= pThis->lastTouchedNodePosition[1]; + proxyPosition[2] -= pThis->lastTouchedNodePosition[2]; + if(pThis->lastTouchedEdgeID != HL_OBJECT_ANY){ + if(norm(proxyPosition) > EDGE_SPEECH_DISTANCE_WHEN_LEAVING_NODE){ + pThis->frictionDisabled = false; + HLboolean isTouching; + hlGetShapeBooleanv(pThis->lastTouchedEdgeID,HL_PROXY_IS_TOUCHING,&isTouching); + if(isTouching == HL_TRUE){ + try{ + CollectionsManager::EdgeData & ed = pThis->collectionsManager->getEdgeDataFromID(pThis->lastTouchedEdgeID); + pThis->executeCommand('t',ed.diagramId,0,0,0,0); + pThis->lastTouchedEdgeID = ed.hapticId; + pThis->lastHighlightElementID = ed.hapticId; + pThis->executeCommand('s',ed.diagramId,0,0,0,0); + }catch(int){ + stopExecution("Button 1 callback: Could not retrieve edge from ID"); + } + } + } + } +} + +#ifdef VIBRATION +void HLCALLBACK HapticManager::vibrationCB(HDdouble force[3], HLcache *cache, void *userdata){ + static const hduVector3Dd direction( 0, 1, 0 ); + HDdouble instRate; + static HDdouble timer = 0; + + HapticManager *pThis = static_cast<HapticManager*>(userdata); + + /* Use the reciprocal of the instantaneous rate as a timer. */ + hdGetDoublev(HD_INSTANTANEOUS_UPDATE_RATE, &instRate); + timer += 1.0 / instRate; + + /* Apply a sinusoidal force in the direction of motion. */ + hduVecScale(force, direction, VIBRATION_AMPLITUDE * sin(timer * VIBRATION_FREQUENCY)); + hdSetDoublev(HD_CURRENT_FORCE, force); +} + +void HLCALLBACK HapticManager::beforeVibrationCB(HLcache *cache, void *userdata){} + +void HLCALLBACK HapticManager::afterVibrationCB(HLcache *cache, void *userdata){} +#endif + +void HapticManager::init(void) throw(HapticException){ + HDErrorInfo error; + + ghHD = hdInitDevice(HD_DEFAULT_DEVICE); + if (HD_DEVICE_ERROR(error = hdGetError())){ + throw HapticException(); + } + + ghHLRC = hlCreateContext(ghHD); + hlMakeCurrent(ghHLRC); + + // Enable optimization of the viewing parameters when rendering geometry for OpenHaptics. + hlEnable(HL_HAPTIC_CAMERA_VIEW); + + hlTouchableFace(HL_FRONT); + wall1Id = hlGenShapes(1); + wall2Id = hlGenShapes(1); + attractToHapticId = HL_OBJECT_ANY; + lastTouchedNodeID = HL_OBJECT_ANY; + lastTouchedEdgeID = HL_OBJECT_ANY; + draggedElementID = HL_OBJECT_ANY; + lastHighlightElementID = HL_OBJECT_ANY; + + // initialize effects + frictionFX = hlGenEffects(1); + vibrationFX = hlGenEffects(1); + attractionFX = hlGenEffects(1); + springFX = hlGenEffects(1); + displayList1 = 0; + displayList2 = 0; + hlEventd(HL_EVENT_MOTION_LINEAR_TOLERANCE,5); + hlEventd(HL_EVENT_MOTION_ANGULAR_TOLERANCE,5); + + // add callbacks + hlAddEventCallback(HL_EVENT_2BUTTONDOWN,HL_OBJECT_ANY,HL_CLIENT_THREAD,hlButton2CB,this); + hlAddEventCallback(HL_EVENT_2BUTTONUP,HL_OBJECT_ANY,HL_CLIENT_THREAD,hlButton2CB,this); + hlAddEventCallback(HL_EVENT_1BUTTONUP,HL_OBJECT_ANY,HL_CLIENT_THREAD,hlButton1CB,this); + hlAddEventCallback(HL_EVENT_1BUTTONDOWN,HL_OBJECT_ANY,HL_CLIENT_THREAD,hlButton1CB,this); + hlAddEventCallback(HL_EVENT_TOUCH, HL_OBJECT_ANY, HL_CLIENT_THREAD, hlTouchCB, this); + hlAddEventCallback(HL_EVENT_UNTOUCH, HL_OBJECT_ANY, HL_CLIENT_THREAD, hlUntouchCB, this); + hlAddEventCallback(HL_EVENT_MOTION, HL_OBJECT_ANY, HL_CLIENT_THREAD, hlMotionCB, this); + +#ifdef VIBRATION + hlBeginFrame(); + hlCallback(HL_EFFECT_START,(HLcallbackProc) beforeVibrationCB, this); + hlCallback(HL_EFFECT_STOP,(HLcallbackProc) afterVibrationCB, this); + hlCallback(HL_EFFECT_COMPUTE_FORCE, (HLcallbackProc) vibrationCB, this); + hlEndFrame(); +#endif +} + +HapticManager::ClickStatus HapticManager::getButton1Status(){ + if(clickStart == 0){ + return NO_CLICK; + }else if(doubleClick){ + /* double click: set everything back to default value and return DOUBLE_CLICK */ + clickStart = 0; + doubleClick = false; + return TWO_CLICK; + }else if(clock() >= clickStart + CLICK_INTERVAL || secondClick) { + /* one click and enough time elapsed, so that it cannot be a double click */ + clickStart = 0; + return ONE_CLICK; + } + return NO_CLICK; +} + +void HapticManager::doButton1Click(bool doubleClick){ + /* DOUBLE CLICK*/ + if(doubleClick){ + if(lastTouchedNodeID != HL_OBJECT_ANY){ + HLboolean isTouching = HL_FALSE; + hlGetShapeBooleanv(lastTouchedNodeID,HL_PROXY_IS_TOUCHING,&isTouching); // priority to nodes + if(isTouching == HL_TRUE||lastTouchedNodeID == lastMovedElement){ + try{ + CollectionsManager::NodeData & nd = collectionsManager->getNodeDataFromID(lastTouchedNodeID); + executeCommand('i',nd.diagramId, 0,0,0,0); + }catch(int){ + stopExecution("Button 1 callback: Could not retrieve node from ID"); + } + }else{ + if(lastTouchedEdgeID != HL_OBJECT_ANY){ + hlGetShapeBooleanv(lastTouchedEdgeID,HL_PROXY_IS_TOUCHING,&isTouching); + if(isTouching == HL_TRUE||lastTouchedNodeID == lastMovedElement){ + try{ + CollectionsManager::EdgeData & ed = collectionsManager->getEdgeDataFromID(lastTouchedEdgeID); + executeCommand('i',ed.diagramId, 0,0,0,0); + }catch(int){ + stopExecution("Button 1 callback: Could not retrieve edge from ID"); + } + } + } + } + } + // this was a double click, thus clean up the data about the dragging + if(springStatus == DRAGGING){ + springStatus = STOP_DRAGGING; + secondClick = false; + } + return; + } + + /* - SINLGE CLICK - */ + if(!secondClick){ // button 1 pressed for the first time + HLboolean isTouching; + /* if the stylus is touching a node then pick it up */ + /* give priority to nodes */ + if(lastTouchedNodeID != HL_OBJECT_ANY){ + hlGetShapeBooleanv(lastTouchedNodeID,HL_PROXY_IS_TOUCHING,&isTouching); + if(isTouching == HL_TRUE || (stillTouchingLastMovedElement&&lastMovedElement == lastTouchedNodeID)){ + draggedElementID = lastTouchedNodeID; + try{ + CollectionsManager::NodeData & nd = collectionsManager->getNodeDataFromID(lastTouchedNodeID); + executeCommand('c',nd.diagramId,0,0,0,0); // pick up command + }catch(int){ + stopExecution("Button 1 callback: Could not retrieve node from ID"); + } + return; + } + } + /* no node is currently touched, check the edges now */ + if(collectionsManager->isEdge(lastTouchedEdgeID)){ + hlGetShapeBooleanv(lastTouchedEdgeID,HL_PROXY_IS_TOUCHING,&isTouching); + if(isTouching == HL_TRUE||(stillTouchingLastMovedElement&&lastMovedElement == lastTouchedEdgeID)){ + draggedElementID = lastTouchedEdgeID; + try{ + CollectionsManager::EdgeData & ed = collectionsManager->getEdgeDataFromID(lastTouchedEdgeID); + executeCommand('c',ed.diagramId,0,0,0,0); // pick up command + }catch(int){ + stopExecution("Button 1 callback: Could not retrieve edge from ID"); + } + } + } + }else{ // button 1 pressed for the second time + if(springStatus != DRAGGING) + return; + springStatus = STOP_DRAGGING; + HLdouble leanPoint[3]; + hlGetDoublev(HL_PROXY_POSITION,leanPoint); + leanPoint[2] = 1; + hduVector3Dd winCoordPoint; + toScreen(hduVector3Dd(leanPoint),winCoordPoint); + + try{ + if(collectionsManager->isNode(draggedElementID)){ + CollectionsManager::NodeData & nd = collectionsManager->getNodeDataFromID(draggedElementID); + movedNode = true; + executeCommand('m',nd.diagramId,winCoordPoint[0],collectionsManager->getScreenHeight() - winCoordPoint[1],0,0); + }else if(collectionsManager->isEdge(draggedElementID)){ // double check necessary for the user might have changed the tab or closed the window before releasing the spring + springStart[2] = 1; + hduVector3Dd startWinCoordPoint; + toScreen(hduVector3Dd(springStart),startWinCoordPoint);//it's an edge, we also need the point where the motion has started + CollectionsManager::EdgeData & ed = collectionsManager->getEdgeDataFromID(draggedElementID); + executeCommand('m', ed.diagramId, + winCoordPoint[0], + collectionsManager->getScreenHeight() - winCoordPoint[1], + startWinCoordPoint[0], + collectionsManager->getScreenHeight() - startWinCoordPoint[1] + ); + } + }catch(int){ + stopExecution("Button 1 callback: Could not retrieve element from ID"); + } + /* with second click we place the element and it becomes the last moved element */ + lastMovedElement = draggedElementID; + secondClick = false; + } +} + +void HapticManager::setAttractTo(jint hapticId){ + if(attractionStatus == NO_ATTRACTION) // starts only if there is no previous attraction being overwritten + attractionStatus = START_ATTRACTION; + attractToHapticId = hapticId; + alreadyTouchingAttractingElement = false; +} + +void HapticManager::pickUp(jint hapticId){ + springStatus = START_DRAGGING; + secondClick = true; + hlGetDoublev(HL_PROXY_POSITION,springStart); +} + +bool HapticManager::wasAlreadyTouchingAttractingElement(){ + return alreadyTouchingAttractingElement; +} + +void HapticManager::draw(void){ + // Start haptic frame. (Must do this before rendering any haptic shapes.) + hlBeginFrame(); + + if(!doneOnce){ + hlStartEffect(HL_EFFECT_FRICTION, frictionFX); + doneOnce = true; + } + + if(wall){ + // Plane shape. + hlPushAttrib(HL_MATERIAL_BIT|HL_TOUCH_BIT); + hlBeginShape(HL_SHAPE_FEEDBACK_BUFFER, wall1Id); + hlMaterialf(HL_FRONT_AND_BACK, HL_STIFFNESS, 1); + hlMaterialf(HL_FRONT, HL_POPTHROUGH, 0 ); + hlMaterialf(HL_FRONT, HL_STATIC_FRICTION, 0); + hlMaterialf(HL_FRONT, HL_DYNAMIC_FRICTION, 0); + hlTouchModel(HL_CONTACT); + hlTouchableFace(HL_FRONT); + drawPlane(-0.005); + hlEndShape(); +#define SECOND_PLANE +#ifdef SECOND_PLANE + hlBeginShape(HL_SHAPE_FEEDBACK_BUFFER, wall2Id); + hlMaterialf(HL_BACK, HL_STIFFNESS, 1); + hlTouchableFace(HL_BACK); + hlMaterialf(HL_FRONT, HL_POPTHROUGH, 0 ); + hlMaterialf(HL_FRONT, HL_STATIC_FRICTION, 0); + hlMaterialf(HL_FRONT, HL_DYNAMIC_FRICTION, 0); + drawPlane(0.005); + hlEndShape(); +#endif + hlPopAttrib(); + } + + // Start a new haptic shape. Use the feedback buffer to capture OpenGL + // geometry for haptic rendering. + + // draw nodes + int size = collectionsManager->getNodesNum(); + + for(int i=0; i< size;i++){ + CollectionsManager::NodeData & nd = collectionsManager->getNodeData(i); + + hlPushAttrib(HL_HINT_BIT); + hlHinti(HL_SHAPE_FEEDBACK_BUFFER_VERTICES, 3); + hlBeginShape(HL_SHAPE_FEEDBACK_BUFFER, nd.hapticId); + hlTouchModel(HL_CONSTRAINT); + hlTouchModelf(HL_SNAP_DISTANCE , 4.0f ); + hlMaterialf(HL_FRONT_AND_BACK, HL_STIFFNESS, 1.0); + hlMaterialf(HL_FRONT_AND_BACK, HL_DAMPING, 0); + + hlMaterialf(HL_FRONT_AND_BACK, HL_STATIC_FRICTION, 0); + hlMaterialf(HL_FRONT_AND_BACK, HL_DYNAMIC_FRICTION, 0); + + //draw the point + glBegin(GL_POINTS); + glVertex3d(nd.x,nd.y,0); + glEnd(); + + // End the shape. + hlEndShape(); + hlPopAttrib(); + + // saves the coorinates for a later use so that for the effect we don't need to cycle again + if(attractionStatus == START_ATTRACTION || attractionStatus == DOING_ATTRACTION){ + if(nd.hapticId == attractToHapticId){ + attractToCoord[0] = nd.x; + attractToCoord[1] = nd.y; + attractToCoord[2] = 0; + } + } + } + + /* draw lines. When a node is moved, edges are not drawn for the very first haptic frame after the shift. * + * This is to address the behaviour of the device which, after a n ode is moved cannot see the styilus * + * touching the edge. As a consequence of that in hlMotionCB() when moving away from a node along an edge * + * no edge name will be spoken and furthermore the pick up botton won't work as the device thinks it's not * + * touching anything. Not drawing the edge on the first frame, seems to address this issue. */ + if(!movedNode) { + size = collectionsManager->getEdgesNum(); + for(int i = 0; i<size; i++){ + CollectionsManager::EdgeData & ed = collectionsManager->getEdgeData(i); + hlPushAttrib(HL_HINT_BIT|HL_MATERIAL_BIT); + hlHinti(HL_SHAPE_FEEDBACK_BUFFER_VERTICES, ed.getSize()); + + hlBeginShape(HL_SHAPE_FEEDBACK_BUFFER, ed.hapticId); + hlTouchModel(HL_CONSTRAINT); + hlTouchModelf(HL_SNAP_DISTANCE, ( (stickyMode && springStatus != DRAGGING && springStatus != START_DRAGGING) ? 20.0f : 3.5f )); + hlMaterialf(HL_FRONT_AND_BACK, HL_STIFFNESS, 1); + + if(ed.stipplePattern == 0xFFFF){ + hlMaterialf(HL_FRONT_AND_BACK, HL_STATIC_FRICTION, 0.5f); + } + + glLineWidth(1.0); + + glBegin(GL_LINES); + for(unsigned int j = 0; j < ed.getSize(); j++){ + for(unsigned int k = j; k < ed.getSize(); k++){ + if(ed.adjMatrix[j][k]){ + if( k >= ed.nodeStart && + (!stickyMode || (springStatus != RELEASED && springStatus != STOP_DRAGGING)) + && attractionStatus != DOING_ATTRACTION && attractionStatus != START_ATTRACTION){ // not a line connecting two edges and stickyMode not enabled + /* it's drawing a line from a point to a node, thus it draws it a little shorter*/ + double hypotenuse = sqrt( (ed.x[j] - ed.x[k])*(ed.x[j] - ed.x[k]) + (ed.y[j] - ed.y[k])*(ed.y[j] - ed.y[k])); + if(hypotenuse == 0) + continue; + double mySin = (ed.y[j] - ed.y[k])/hypotenuse; + double myCos = (ed.x[j] - ed.x[k])/hypotenuse; + glVertex3d(ed.x[k]+(EDGE_SHORTEND_VAL * myCos) , ed.y[k]+(EDGE_SHORTEND_VAL*mySin),0); + if(j >= ed.nodeStart) //its a direct edge connecting two nodes + glVertex3d(ed.x[j]+(EDGE_SHORTEND_VAL * -myCos),ed.y[j]+(EDGE_SHORTEND_VAL*-mySin),0); + else + glVertex3d(ed.x[j],ed.y[j],0); + }else{ + glVertex3d(ed.x[j],ed.y[j],0); + glVertex3d(ed.x[k],ed.y[k],0); + } + } + } + } + glEnd(); + hlEndShape(); + hlPopAttrib(); + + // saves the coorinates for a later use so that for the effect we don't need to cycle again + if(attractionStatus == START_ATTRACTION || attractionStatus == DOING_ATTRACTION){ + if(ed.hapticId == attractToHapticId){ + attractToCoord[0] = ed.attractPoint[0]; + attractToCoord[1] = ed.attractPoint[1]; + attractToCoord[2] = 0; + } + } + } + }else { + movedNode = false; + } + /* --- FORCES --- */ + /* friction */ + if(lastTouchedEdgeID != HL_OBJECT_ANY){ + /* check if the edge is still there, it might not as the tab could be changed */ + if(collectionsManager->isEdge(lastTouchedEdgeID)){ + //hlGetShapeBooleanv(lastTouchedEdgeID,HL_PROXY_IS_TOUCHING,&isTouching); FIXME to remove + if(frictionDisabled||lastTouchedEdgeIsUntouched/*(isTouching == HL_FALSE)*/){ + hlEffectd(HL_EFFECT_PROPERTY_MAGNITUDE, 0); + hlEffectd(HL_EFFECT_PROPERTY_GAIN, 0); + }else{ + if(lastTouchedEdgeStipplePattern == 0xAAAA){ // dotted + hlEffectd(HL_EFFECT_PROPERTY_MAGNITUDE, 0.4f); + hlEffectd(HL_EFFECT_PROPERTY_GAIN, 1); + }else if(lastTouchedEdgeStipplePattern == 0xF0F0){ // dashed + hlEffectd(HL_EFFECT_PROPERTY_MAGNITUDE, 0.6); + hlEffectd(HL_EFFECT_PROPERTY_GAIN, 0.6); + } + } + }else{ + lastTouchedEdgeID = HL_OBJECT_ANY; + hlEffectd(HL_EFFECT_PROPERTY_MAGNITUDE, 0); + hlEffectd(HL_EFFECT_PROPERTY_GAIN, 0); + } + hlUpdateEffect(frictionFX); + } + + // spring + if(springStatus == START_DRAGGING){ + hlPushAttrib(HL_EFFECT_BIT); + hlEffectd(HL_EFFECT_PROPERTY_GAIN, 0.3); + hlEffectd(HL_EFFECT_PROPERTY_MAGNITUDE, 0.5); + + hduMatrix worldToView; + hduMatrix graphicToTouch; + hduMatrix touchToWorkspace; + hduMatrix WorldToDevice; + glMatrixMode(GL_MODELVIEW); + glGetDoublev(GL_MODELVIEW_MATRIX,worldToView); + hlGetDoublev(HL_VIEWTOUCH_MATRIX, graphicToTouch ); + hlGetDoublev(HL_TOUCHWORKSPACE_MATRIX, touchToWorkspace ); + WorldToDevice = worldToView * graphicToTouch * touchToWorkspace; + + hduVector3Dd dst; + WorldToDevice.multVecMatrix(hduVector3Dd(springStart[0],springStart[1],0),dst); + + hlEffectdv(HL_EFFECT_PROPERTY_POSITION, dst); + hlStartEffect(HL_EFFECT_SPRING, springFX); + hlPopAttrib(); + springStatus = DRAGGING; + }else if(springStatus == STOP_DRAGGING){ + hlStopEffect(springFX); + springStatus = RELEASED; + } + + // attraction + HLboolean isTouching = HL_FALSE; + if(attractionStatus == DOING_ATTRACTION || attractionStatus == START_ATTRACTION){ + /* we need to be sure we're currently touching the element */ + if(attractToHapticId != HL_OBJECT_ANY){ + hlGetShapeBooleanv(attractToHapticId,HL_PROXY_IS_TOUCHING,&isTouching); + } + + // when touching the element we're getting attracted to, then shut the force down + if(isTouching == HL_TRUE){ + if(attractionStatus == DOING_ATTRACTION){ // we finally reached the attracting node + attractionStatus = STOP_ATTRACTION; + }else{ + attractionStatus = NO_ATTRACTION; // we were already touching the node we wanted to get attracted to + alreadyTouchingAttractingElement = true; + } + } + } + + if(attractionStatus == STOP_ATTRACTION){ + attractionStatus = NO_ATTRACTION; + hlStopEffect(attractionFX); + } else if(attractionStatus == START_ATTRACTION || attractionStatus == DOING_ATTRACTION){ + HLdouble devicePosition[3]; + HDdouble direction[3]; + hlGetDoublev(HL_DEVICE_POSITION,devicePosition); + direction[0] = attractToCoord[0] - devicePosition[0]; + direction[1] = attractToCoord[1] - devicePosition[1]; + direction[2] = attractToCoord[2] - devicePosition[2]; + HDdouble directionNorm = norm(direction); + if(directionNorm != 0 ){ + direction[0] /= directionNorm; + direction[1] /= directionNorm; + direction[2] /= directionNorm; + } + hlPushAttrib(HL_EFFECT_BIT); + hlEffectdv(HL_EFFECT_PROPERTY_DIRECTION, direction); + hlEffectd(HL_EFFECT_PROPERTY_GAIN, 0.5f); + hlEffectd(HL_EFFECT_PROPERTY_MAGNITUDE, 0.5f); + if(attractionStatus == START_ATTRACTION){ + hlStartEffect(HL_EFFECT_CONSTANT, attractionFX); + attractionStatus = DOING_ATTRACTION; + }else{ + hlUpdateEffect(attractionFX); + } + hlPopAttrib(); + } +#ifdef VIBRATION + if(vibrationStatus == START_VIBRATION){ + hlStartEffect(HL_EFFECT_CALLBACK, vibrationFX); + vibrationEnd = clock() + VIBRATION_DURATION; + vibrationStatus = DOING_VIBRATION; + }else if(vibrationStatus == DOING_VIBRATION){ + if(clock() >= vibrationEnd){ + hlStopEffect(vibrationFX); + vibrationStatus = NO_VIBRATION; + } + } +#endif + // End the haptic frame. + hlEndFrame(); +} + +void HapticManager::drawPlane(float zoffset){ + GLuint displayList = (zoffset < 0) ? displayList1 : displayList2; + + if (displayList){ + glCallList(displayList); + } + else{ + if(zoffset < 0){ + displayList1 = glGenLists(1); + glNewList(displayList1, GL_COMPILE_AND_EXECUTE); + }else{ + displayList2 = glGenLists(1); + glNewList(displayList2, GL_COMPILE_AND_EXECUTE); + } + glNewList(displayList, GL_COMPILE_AND_EXECUTE); + glPushAttrib(GL_ENABLE_BIT); + glPolygonOffset(1,1); + glEnable(GL_POLYGON_OFFSET_FILL); + + glBegin(GL_QUADS); + glVertex3f(-5, -5, zoffset); + glVertex3f(5, -5, zoffset); + glVertex3f(5, 5, zoffset); + glVertex3f(-5, 5, zoffset); + glEnd(); + glDisable(GL_POLYGON_OFFSET_FILL); + glPopAttrib(); + glEndList(); + } + +} + +HHD HapticManager::ghHD = HD_INVALID_HANDLE; +HHLRC HapticManager::ghHLRC = 0; +const double HapticManager::EDGE_SHORTEND_VAL = 0.06; +const unsigned int HapticManager::CLICK_INTERVAL = CLOCKS_PER_SEC/5; +const double HapticManager::EDGE_SPEECH_DISTANCE_WHEN_LEAVING_NODE = 0.03; +#ifdef VIBRATION +const HDdouble HapticManager::VIBRATION_AMPLITUDE = 0.88;//0.88; +const HDint HapticManager::VIBRATION_FREQUENCY = 170;//160; +const unsigned int HapticManager::VIBRATION_DURATION = CLOCKS_PER_SEC/10; // = 100 ms +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/HapticManager.h Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,122 @@ +#pragma once +#include "stdAfx.h" +#include <time.h> +#include "HapticException.h" +#include "CollectionsManager.h" + +#define NOVIBRATION + +/* The haptic manager draws the diagram openGL scene haptically. * + * In order to get all the data about the diagram it's constructed * + * passing a reference to the CollectionManager */ +class HapticManager +{ + enum AttractionStatus {START_ATTRACTION, DOING_ATTRACTION, NO_ATTRACTION, STOP_ATTRACTION}; + enum SpringStatus { DRAGGING, RELEASED, START_DRAGGING, STOP_DRAGGING }; + enum VibrationStatus { START_VIBRATION , DOING_VIBRATION , NO_VIBRATION, CAN_VIBRATE}; + + + /* Haptic device and rendering context handles. */ + HLuint wall1Id; + HLuint wall2Id; + HLuint frictionFX; + HLuint vibrationFX; + HLuint attractionFX; + HLuint springFX; + VibrationStatus vibrationStatus; + AttractionStatus attractionStatus; + SpringStatus springStatus; +#ifdef VIBRATION + static const HDdouble VIBRATION_AMPLITUDE; + static const HDint VIBRATION_FREQUENCY; + static const unsigned int VIBRATION_DURATION; +#endif + static const double EDGE_SHORTEND_VAL; + static const double EDGE_SPEECH_DISTANCE_WHEN_LEAVING_NODE; + static const unsigned int CLICK_INTERVAL; + clock_t clickStart; + clock_t vibrationEnd; + HDdouble forceBeforeVibration[3]; + CollectionsManager *collectionsManager; + bool wall; + jint attractToHapticId; + bool alreadyTouchingAttractingElement; + jdouble attractToCoord[3]; + // this is needed to calulate the distance between the proxy and the last touched node + HLdouble lastTouchedNodePosition[3]; + HLint lastTouchedNodeID; + HLint lastTouchedEdgeID; + jint lastTouchedEdgeStipplePattern; + HLint lastHighlightElementID; + HLint draggedElementID; + /* when dropping a node after moving it, unless one detouches and touches again the node, + the hlGetShapeBooleanv(HL_PROXY_IS_TOUCHING) routine will return a false value when + one tries to hook the node again. Even tough the touch and untouch callback are called + correctly. this variable keeps track of the last moved element, setting it to HL_OBJECT_ANY + on the detouch callback so that this "buggy" behaviour is worked around + */ + HLuint lastMovedElement; + bool stillTouchingLastMovedElement; + /* this variable is to workaround the HL_PROXY_IS_TOUCHNIG problem to fix issue of stipple pattern: + when dropping a node after having picked it up, then the hlGetShapeBooleanv may return false + for all the edges attached to a moved node or for a moved edge. Because of that the stipple + pattern for dotted and dashed lines might not be enabled. The check is however necessary to stop the + effect when actually untouching the node or when changing tab after the pick up and drop. + The variable is set/unset in the touching callbacks which works fine. + */ + bool lastTouchedEdgeIsUntouched; + HLdouble springStart[3]; + GLuint displayList1; + GLuint displayList2; + bool frictionDisabled; + bool doneOnce; + bool secondClick; + bool stickyMode; + bool doubleClick; + void drawPlane(float offset); + void (*executeCommand)(const jchar cmd, const jint ID, const jdouble startx, const jdouble starty, const jdouble endx, const jdouble endy); + + static void HLCALLBACK hlButton1CB(HLenum event, HLuint object, HLenum thread, + HLcache *cache, void *userdata); + static void HLCALLBACK hlButton2CB(HLenum event, HLuint object, HLenum thread, + HLcache *cache, void *userdata); + static void HLCALLBACK hlTouchCB(HLenum event, HLuint object, HLenum thread, + HLcache *cache, void *userdata); + static void HLCALLBACK hlUntouchCB(HLenum event, HLuint object, HLenum thread, + HLcache *cache, void *userdata); + static void HLCALLBACK hlMotionCB(HLenum event, HLuint object, HLenum thread, + HLcache *cache, void *userdata); +#ifdef VIBRATION + static void HLCALLBACK vibrationCB(HDdouble force[3], HLcache *cache, void *userdata); + + static void HLCALLBACK beforeVibrationCB(HLcache *cache, void *userdata); + + static void HLCALLBACK afterVibrationCB(HLcache *cache, void *userdata); +#endif + +public: + + enum ClickStatus { NO_CLICK, ONE_CLICK, TWO_CLICK }; + + static HHD ghHD; + static HHLRC ghHLRC; + HapticManager(CollectionsManager * cManager, void (*func)(const jchar cmd, const jint ID, const jdouble startx, const jdouble starty, const jdouble endx, const jdouble endy)) + : executeCommand(func), wall(true), collectionsManager(cManager), vibrationStatus(NO_VIBRATION), springStatus(RELEASED), alreadyTouchingAttractingElement(false), + doneOnce(false), attractionStatus(NO_ATTRACTION), frictionDisabled(false), secondClick(false), clickStart(0), stickyMode(true), + lastMovedElement(HL_OBJECT_ANY),stillTouchingLastMovedElement(false),lastTouchedEdgeIsUntouched(false){}; + ~HapticManager(void) {} + void init(void) throw(HapticException); + void draw(void); + void setAttractTo(jint hapticId); + void pickUp(jint hapticId); + bool wasAlreadyTouchingAttractingElement(void); + + void enableWall(void){ wall = true; } + void disableWall(void){ wall = false; } + + bool isDraggingNode() const { return springStatus == DRAGGING && collectionsManager->isNode(draggedElementID); } + bool isDraggingEdge() const { return springStatus == DRAGGING && collectionsManager->isEdge(draggedElementID); } + ClickStatus getButton1Status(); + void doButton1Click(bool doubleClick); + +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/Haptics.vcproj Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,504 @@ +<?xml version="1.0" encoding="gb2312"?> +<VisualStudioProject + ProjectType="Visual C++" + Version="9.00" + Name="Haptics" + ProjectGUID="{3FAB66F5-BA54-470F-9D4B-3E73E58A76BC}" + RootNamespace="Haptics" + TargetFrameworkVersion="131072" + > + <Platforms> + <Platform + Name="Win32" + /> + <Platform + Name="x64" + /> + </Platforms> + <ToolFiles> + </ToolFiles> + <Configurations> + <Configuration + Name="Release|Win32" + OutputDirectory="$(SolutionDir)$(ConfigurationName)" + IntermediateDirectory=".\Release" + ConfigurationType="2" + InheritedPropertySheets="$(VCInstallDir)VCProjectDefaults\UpgradeFromVC71.vsprops" + UseOfMFC="0" + ATLMinimizesCRunTimeLibraryUsage="false" + CharacterSet="2" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + TypeLibraryName=".\Release/HelloSphere.tlb" + HeaderFileName="" + /> + <Tool + Name="VCCLCompilerTool" + Optimization="2" + InlineFunctionExpansion="1" + AdditionalIncludeDirectories=""C:\Program Files\Java\jdk1.6.0_25\include";"C:\Program Files\Java\jdk1.6.0_25\include\win32";include;"$(3DTOUCH_BASE)\include";"$(3DTOUCH_BASE)\utilities\include"" + PreprocessorDefinitions="WIN32;NDEBUG;_CONSOLE" + StringPooling="true" + RuntimeLibrary="2" + EnableFunctionLevelLinking="true" + PrecompiledHeaderFile=".\Release/HelloSphere.pch" + AssemblerListingLocation=".\Release/" + ObjectFile=".\Release/" + ProgramDataBaseFileName=".\Release/" + WarningLevel="3" + SuppressStartupBanner="true" + CompileAs="0" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + PreprocessorDefinitions="NDEBUG" + Culture="1033" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLinkerTool" + AdditionalDependencies="hl.lib hlu.lib hd.lib hdu.lib glut32.lib" + LinkIncremental="1" + SuppressStartupBanner="true" + AdditionalLibraryDirectories=""$(3DTOUCH_BASE)\lib\$(PlatformName)\$(ConfigurationName)";"$(3DTOUCH_BASE)\utilities\lib\$(PlatformName)\$(ConfigurationName)";"$(3DTOUCH_BASE)\lib\$(PlatformName)\"" + ProgramDatabaseFile=".\Release/HelloSphere.pdb" + SubSystem="1" + RandomizedBaseAddress="1" + DataExecutionPrevention="0" + 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="Debug|Win32" + OutputDirectory=".\Debug" + IntermediateDirectory=".\Debug" + ConfigurationType="2" + InheritedPropertySheets="$(VCInstallDir)VCProjectDefaults\UpgradeFromVC71.vsprops" + UseOfMFC="0" + ATLMinimizesCRunTimeLibraryUsage="false" + CharacterSet="2" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + TypeLibraryName=".\Debug/HelloSphere.tlb" + HeaderFileName="" + /> + <Tool + Name="VCCLCompilerTool" + Optimization="0" + AdditionalIncludeDirectories=""C:\Program Files\Java\jdk1.6.0_24\include\win32";"C:\Program Files\Java\jdk1.6.0_24\include";include;"$(3DTOUCH_BASE)\include";"$(3DTOUCH_BASE)\utilities\include"" + PreprocessorDefinitions="WIN32;_DEBUG;_CONSOLE" + BasicRuntimeChecks="3" + RuntimeLibrary="3" + PrecompiledHeaderFile=".\Debug/HelloSphere.pch" + AssemblerListingLocation=".\Debug/" + ObjectFile=".\Debug/" + ProgramDataBaseFileName=".\Debug/" + WarningLevel="3" + SuppressStartupBanner="true" + DebugInformationFormat="4" + CompileAs="0" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + PreprocessorDefinitions="_DEBUG" + Culture="1033" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLinkerTool" + AdditionalDependencies="hl.lib hlu.lib hd.lib hdu.lib glut32.lib" + OutputFile=".\Debug/HelloSphere.exe" + LinkIncremental="1" + SuppressStartupBanner="true" + AdditionalLibraryDirectories=""$(3DTOUCH_BASE)\lib\$(PlatformName)\$(ConfigurationName)";"$(3DTOUCH_BASE)\utilities\lib\$(PlatformName)\$(ConfigurationName)";"$(3DTOUCH_BASE)\lib\$(PlatformName)\"" + GenerateDebugInformation="true" + ProgramDatabaseFile=".\Debug/HelloSphere.pdb" + SubSystem="1" + RandomizedBaseAddress="1" + DataExecutionPrevention="0" + 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|x64" + OutputDirectory="$(PlatformName)\$(ConfigurationName)" + IntermediateDirectory="$(PlatformName)\$(ConfigurationName)" + ConfigurationType="1" + InheritedPropertySheets="$(VCInstallDir)VCProjectDefaults\UpgradeFromVC71.vsprops" + UseOfMFC="0" + ATLMinimizesCRunTimeLibraryUsage="false" + CharacterSet="2" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + TargetEnvironment="3" + TypeLibraryName=".\Release/HelloSphere.tlb" + HeaderFileName="" + /> + <Tool + Name="VCCLCompilerTool" + Optimization="2" + InlineFunctionExpansion="1" + AdditionalIncludeDirectories="include,$(3DTOUCH_BASE)\include,$(3DTOUCH_BASE)\utilities\include" + PreprocessorDefinitions="WIN32;NDEBUG;_CONSOLE" + StringPooling="true" + RuntimeLibrary="2" + EnableFunctionLevelLinking="true" + PrecompiledHeaderFile=".\Release/HelloSphere.pch" + AssemblerListingLocation=".\Release/" + ObjectFile=".\Release/" + ProgramDataBaseFileName=".\Release/" + WarningLevel="3" + SuppressStartupBanner="true" + CompileAs="0" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + PreprocessorDefinitions="NDEBUG" + Culture="1033" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLinkerTool" + AdditionalDependencies="hl.lib hlu.lib hd.lib hdu.lib glut32.lib" + OutputFile="$(OutDir)\$(ProjectName).exe" + LinkIncremental="1" + SuppressStartupBanner="true" + AdditionalLibraryDirectories=""$(3DTOUCH_BASE)\lib\$(PlatformName)\$(ConfigurationName)";"$(3DTOUCH_BASE)\utilities\lib\$(PlatformName)\$(ConfigurationName)";"$(3DTOUCH_BASE)\lib\$(PlatformName)\"" + ProgramDatabaseFile=".\Release/HelloSphere.pdb" + SubSystem="1" + TargetMachine="17" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCManifestTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCAppVerifierTool" + /> + <Tool + Name="VCWebDeploymentTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + <Configuration + Name="Debug|x64" + OutputDirectory="$(PlatformName)\$(ConfigurationName)" + IntermediateDirectory="$(PlatformName)\$(ConfigurationName)" + ConfigurationType="1" + InheritedPropertySheets="$(VCInstallDir)VCProjectDefaults\UpgradeFromVC71.vsprops" + UseOfMFC="0" + ATLMinimizesCRunTimeLibraryUsage="false" + CharacterSet="2" + > + <Tool + Name="VCPreBuildEventTool" + /> + <Tool + Name="VCCustomBuildTool" + /> + <Tool + Name="VCXMLDataGeneratorTool" + /> + <Tool + Name="VCWebServiceProxyGeneratorTool" + /> + <Tool + Name="VCMIDLTool" + TargetEnvironment="3" + TypeLibraryName=".\Debug/HelloSphere.tlb" + HeaderFileName="" + /> + <Tool + Name="VCCLCompilerTool" + Optimization="0" + AdditionalIncludeDirectories="include,$(3DTOUCH_BASE)\include,$(3DTOUCH_BASE)\utilities\include" + PreprocessorDefinitions="WIN32;_DEBUG;_CONSOLE" + BasicRuntimeChecks="3" + RuntimeLibrary="3" + PrecompiledHeaderFile=".\Debug/HelloSphere.pch" + AssemblerListingLocation=".\Debug/" + ObjectFile=".\Debug/" + ProgramDataBaseFileName=".\Debug/" + WarningLevel="3" + SuppressStartupBanner="true" + DebugInformationFormat="3" + CompileAs="0" + /> + <Tool + Name="VCManagedResourceCompilerTool" + /> + <Tool + Name="VCResourceCompilerTool" + PreprocessorDefinitions="_DEBUG" + Culture="1033" + /> + <Tool + Name="VCPreLinkEventTool" + /> + <Tool + Name="VCLinkerTool" + AdditionalDependencies="hl.lib hlu.lib hd.lib hdu.lib glut32.lib" + OutputFile="$(OutDir)\$(ProjectName).exe" + LinkIncremental="1" + SuppressStartupBanner="true" + AdditionalLibraryDirectories=""$(3DTOUCH_BASE)\lib\$(PlatformName)\$(ConfigurationName)";"$(3DTOUCH_BASE)\utilities\lib\$(PlatformName)\$(ConfigurationName)";"$(3DTOUCH_BASE)\lib\$(PlatformName)\"" + GenerateDebugInformation="true" + ProgramDatabaseFile=".\Debug/HelloSphere.pdb" + SubSystem="1" + TargetMachine="17" + /> + <Tool + Name="VCALinkTool" + /> + <Tool + Name="VCManifestTool" + /> + <Tool + Name="VCXDCMakeTool" + /> + <Tool + Name="VCBscMakeTool" + /> + <Tool + Name="VCFxCopTool" + /> + <Tool + Name="VCAppVerifierTool" + /> + <Tool + Name="VCWebDeploymentTool" + /> + <Tool + Name="VCPostBuildEventTool" + /> + </Configuration> + </Configurations> + <References> + </References> + <Files> + <Filter + Name="Source Files" + Filter="cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" + > + <File + RelativePath=".\CollectionsManager.cpp" + > + </File> + <File + RelativePath=".\GraphicManager.cpp" + > + </File> + <File + RelativePath=".\HapticException.cpp" + > + </File> + <File + RelativePath=".\HapticManager.cpp" + > + </File> + <File + RelativePath=".\stdafx.cpp" + > + </File> + <File + RelativePath=".\uk_ac_qmul_eecs_ccmi_haptics_Haptics.cpp" + > + <FileConfiguration + Name="Release|Win32" + > + <Tool + Name="VCCLCompilerTool" + Optimization="2" + AdditionalIncludeDirectories="" + PreprocessorDefinitions="" + /> + </FileConfiguration> + <FileConfiguration + Name="Debug|Win32" + > + <Tool + Name="VCCLCompilerTool" + Optimization="0" + AdditionalIncludeDirectories="" + PreprocessorDefinitions="" + BasicRuntimeChecks="3" + /> + </FileConfiguration> + <FileConfiguration + Name="Release|x64" + > + <Tool + Name="VCCLCompilerTool" + Optimization="2" + AdditionalIncludeDirectories="" + PreprocessorDefinitions="" + /> + </FileConfiguration> + <FileConfiguration + Name="Debug|x64" + > + <Tool + Name="VCCLCompilerTool" + Optimization="0" + AdditionalIncludeDirectories="" + PreprocessorDefinitions="" + BasicRuntimeChecks="3" + /> + </FileConfiguration> + </File> + <File + RelativePath=".\utils.cpp" + > + </File> + </Filter> + <Filter + Name="Header Files" + Filter="h;hpp;hxx;hm;inl" + > + <File + RelativePath=".\CollectionsManager.h" + > + </File> + <File + RelativePath=".\GraphicManager.h" + > + </File> + <File + RelativePath=".\HapticException.h" + > + </File> + <File + RelativePath=".\HapticManager.h" + > + </File> + <File + RelativePath=".\stdafx.h" + > + </File> + <File + RelativePath=".\uk_ac_qmul_eecs_ccmi_haptics_Haptics.h" + > + </File> + <File + RelativePath=".\utils.h" + > + </File> + </Filter> + <Filter + Name="Resource Files" + Filter="ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" + > + </Filter> + </Files> + <Globals> + </Globals> +</VisualStudioProject>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/stdafx.cpp Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,2 @@ + +#include "stdafx.h" \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/stdafx.h Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,20 @@ +#ifndef stdAfx_H_ +#define stdAfx_H_ + +#if defined(WIN32) +#include <windows.h> +#endif + +#if defined(WIN32) || defined(linux) +#include <GL/glut.h> +#elif defined(__APPLE__) +#include <GLUT/glut.h> +#endif + +#include <HL/hl.h> +#include <HDU/hduMatrix.h> +#include <HDU/hduError.h> + +#include <HLU/hlu.h> + +#endif //stdAfx_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/uk_ac_qmul_eecs_ccmi_haptics_Haptics.cpp Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,518 @@ +#include "uk_ac_qmul_eecs_ccmi_haptics_Haptics.h" +#include "stdafx.h" +#include "GraphicManager.h" +#include "HapticManager.h" +#include "HapticException.h" +#include <stdlib.h> +#include <math.h> +#include <assert.h> +#include <setjmp.h> +#include "utils.h" + +#define CURSOR_SIZE_PIXELS 20 + +enum {JMP_OK =0, JMP_EXIT }; + +/************************** + Function prototypes. + ***************************/ +/* callbacks */ +void displayCallback(void); +void reshapeCallback(int width, int height); +void idleCallback(void); +void exitProcedure(void); +void hapticCommandCB(const jchar cmd, const jint ID, const jdouble x, const jdouble y,const jdouble startX, const jdouble startY); + +void drawCursor(); +void updateWorkspace(); + +void initJniVariables(void); +/************************* + global variables + **************************/ +GraphicManager *gManager; +HapticManager *hManager; +CollectionsManager *cManager; +JNIEnv *env; +jobject *lock; +int width; +int height; +jmp_buf jmpenv; +int jmpval = 0; +/* jni variables */ +jclass hapticClass; +jclass hapticListenerClass; +jfieldID shutdownfieldId; +jfieldID newHapticIdfieldId; +jfieldID dumpHapticIdfieldId; +jfieldID currentHapticIdfieldId; +jfieldID hapticInitFailedfieldId; +jfieldID attractTofieldId; +jfieldID attractToHapticIdFieldId; +jfieldID pickUpfieldId; +jfieldID pickUpHapticIdFieldId; +jfieldID hapticListenerFieldId; +jfieldID cmdFieldId; // belongs to the haptic listener +jfieldID diagramElementFieldId; // belongs to the haptic listener +jfieldID xFieldId; // belongs to the haptic listener +jfieldID yFieldId; // belongs to the haptic listener +jfieldID startXFieldId; // belongs to the haptic listener +jfieldID startYFieldId; // belongs to the haptic listener +jobject hapticListener; +jmethodID notifyMethodId; +jmethodID notifyListenerMethodId; +jmethodID waitMethodId; + +/******************************************************************************* + Initializes GLUT for displaying a simple haptic scene. +*******************************************************************************/ +JNIEXPORT jint JNICALL Java_uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics_init + (JNIEnv *environment, jobject obj, jint w, jint h){ + env = environment; + lock = &obj; + /* fake main argv and argc as this is a dll */ + char *argv[1] = {"OmniHaptics"}; + int argc = 1; + + initJniVariables(); + + /* glut initialization */ + glutInit(&argc, argv); + glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); + glutInitWindowSize(w, h); + glutCreateWindow("CCmI Diagram Haptics"); + + /* glut callbacks */ + glutDisplayFunc(displayCallback); + glutReshapeFunc(reshapeCallback); + glutIdleFunc(idleCallback); + + cManager = new CollectionsManager(env,lock); + hManager = new HapticManager(cManager,hapticCommandCB); + gManager = new GraphicManager(cManager,hManager); + cManager->init(); + gManager->init(); + + // try to initialize the haptic, tell the java thread about the success/failure. The endeavour + // takes the lock on the haptics java object in order to access the nodes and edges concurrently + if(env->MonitorEnter(*lock) != JNI_OK){ + stopExecution("Could not allocate memory for haptic thread monitor"); + } + checkExceptions(env, "Could not enter monitor on Haptics"); + + bool mustSayGoodbye = false; + try{ + hManager->init(); + }catch (HapticException e){ + env->SetBooleanField(*lock,hapticInitFailedfieldId,JNI_TRUE); + mustSayGoodbye = true; + } + + if(mustSayGoodbye) + std::cout << "Failed to initialize haptic device" << std::endl; + else + std::cout << "Haptic device successfully initialized" << std::endl; + + // notify the other thread + env->CallVoidMethod(*lock,notifyMethodId); + checkExceptions(env,"Could not call notify() on Haptics"); + + //release the lock + if(env->MonitorExit(*lock) != JNI_OK){ + std::cerr << "Could not release memory for haptic thread monitor" << std::endl; + exit(-1); + } + + if(mustSayGoodbye) + /* initialization failed: return */ + return -1; + + /* use setjmp to be able to jump off the glutMainLoop when the user shuts the program down */ + jmpval = setjmp(jmpenv); + + /* star the loop*/ + if(jmpval == JMP_OK){ + glutMainLoop(); + }else{ + exitProcedure(); + } + return 0; +} + +/******************************************************************************* + GLUT callback for redrawing the view. +*******************************************************************************/ +void displayCallback(){ + + // takes the lock on the haptics java obejct in order to access the nodes and edges concurrently + if(env->MonitorEnter(*lock) != JNI_OK){ + stopExecution("Could not allocate memory for haptic thread monitor"); + } + checkExceptions(env,"Could not enter monitor on Haptics"); + + // check if there is a shutdown request + if( env->GetBooleanField(*lock,shutdownfieldId) == JNI_TRUE ){ + // notify the other thread that this thread is about to die + env->CallVoidMethod(*lock,notifyMethodId); + checkExceptions(env, "Could not call notify() on Haptics"); + // release the lock + if(env->MonitorExit(*lock) != JNI_OK){ + std::cerr << "Could not release memory for haptic thread monitor" << std::endl; + } + longjmp(jmpenv,JMP_EXIT); + } + + // check if the user asked to be attracted to a node + jboolean attractTo = env->GetBooleanField(*lock,attractTofieldId); + jint attractToDiagramId = 0; + if(attractTo == JNI_TRUE){ + env->SetBooleanField(*lock,attractTofieldId,JNI_FALSE); + jint attractToHapticId = env->GetIntField(*lock,attractToHapticIdFieldId); + if(cManager->isNode(attractToHapticId)){ + attractToDiagramId = cManager->getNodeDataFromID(attractToHapticId).diagramId; + }else{ + attractToDiagramId = cManager->getEdgeDataFromID(attractToHapticId).diagramId; + } + hManager->setAttractTo(attractToHapticId); + } + // check if the user picked up a node + jboolean pickUp = env->GetBooleanField(*lock,pickUpfieldId); + jint pickUpDiagramId = 0; + if(pickUp == JNI_TRUE){ + env->SetBooleanField(*lock,pickUpfieldId,JNI_FALSE); + jint pickUpHapticId = env->GetIntField(*lock,pickUpHapticIdFieldId); + if(cManager->isNode(pickUpHapticId)){ + pickUpDiagramId = cManager->getNodeDataFromID(pickUpHapticId).diagramId; + }else{ + pickUpDiagramId = cManager->getEdgeDataFromID(pickUpHapticId).diagramId; + } + hManager->pickUp(pickUpDiagramId); + } + + // draw the scene graphically and haptically + hManager->draw(); + gManager->draw(); + + // check whether the java thread needs to either create or dump an haptic id + jboolean needsNewHapticId = env->GetBooleanField(*lock,newHapticIdfieldId); + if(needsNewHapticId == JNI_TRUE){ + // set int currentHapticId of class Haptics with a new generated id + jint newHapticid = hlGenShapes(1); + env->SetIntField(*lock, currentHapticIdfieldId, newHapticid); + //set the boolean field to false as the other thread now has an id + env->SetBooleanField(*lock, newHapticIdfieldId, JNI_FALSE); + //notify the other thread + env->CallVoidMethod(*lock,notifyMethodId); + checkExceptions(env, "Could not call notify() on Haptics"); + } + jboolean needsDumpOldHapticId = env->GetBooleanField(*lock, dumpHapticIdfieldId); + if(needsDumpOldHapticId == JNI_TRUE){ + // get the id of the deleted element from the other thread + jint oldHapticId = env->GetIntField(*lock,currentHapticIdfieldId); + // free the old haptic id + hlDeleteShapes(oldHapticId,1); + // set the boolean field as the id has been cleaned up + env->SetBooleanField(*lock,dumpHapticIdfieldId,JNI_FALSE); + // notify the other thread + env->CallVoidMethod(*lock,notifyMethodId); + checkExceptions(env, "Could not call notify() on Haptics"); + } + + /* release lock */ + if(env->MonitorExit(*lock) != JNI_OK){ + stopExecution("Could not release memory for haptic thread monitor"); + } + + /* it's important that this call be outside the monitors, else a deadlock occurs */ + // Call any event callbacks that have been triggered. + if(attractTo == JNI_TRUE){ + if(hManager->wasAlreadyTouchingAttractingElement()){ + hapticCommandCB('t',attractToDiagramId,0,0,0,0); + } + } + + hlCheckEvents(); + HapticManager::ClickStatus click = hManager->getButton1Status(); + if(click == HapticManager::ONE_CLICK){ + hManager->doButton1Click(false); + }else if(click == HapticManager::TWO_CLICK){ + hManager->doButton1Click(true); + } + + glutSwapBuffers(); +} +/******************************************************************************* + GLUT callback for reshaping the window. This is the main place where the + viewing and workspace transforms get initialized. +*******************************************************************************/ +void reshapeCallback(int w, int h){ + static const double kPI = 3.1415926535897932384626433832795; + static const double kFovY = 40; + + static const double nearDist = 1.0 / tan((kFovY / 2.0) * kPI / 180.0 /* radiants for 1 degree */); + static const double farDist = nearDist + 2.0; + double aspect; + width = w; + height = h; + + cManager->setScreenHeight(h); + glViewport(0, 0, width, height); + // Compute the viewing parameters based on a fixed fov and viewing + // a canonical box centered at the origin. + aspect = (double) width/height ; + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluPerspective(kFovY, aspect,nearDist, farDist); + // Place the camera down the Z axis looking at the origin. + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + gluLookAt(0, 0, nearDist + 1.0, + 0, 0, 0, + 0, 1, 0); + + hduVector3Dd origin; + fromScreen(hduVector3Dd(0, 0, 0),origin); + glLoadIdentity(); + gluLookAt(-origin[0],origin[1], nearDist + 1.0, + -origin[0],origin[1], 0, + 0, 1, 0); + + hduVector3Dd end; + fromScreen(hduVector3Dd(w, h, 0),end); + + //glTranslatef(end[0]-origin[0],-(end[1]-origin[1]),0); + + updateWorkspace(); +} + + +/******************************************************************************* + Use the current OpenGL viewing transforms to initialize a transform for the + haptic device workspace so that it's properly mapped to world coordinates. +*******************************************************************************/ +void updateWorkspace(){ + GLdouble modelview[16]; + GLdouble projection[16]; + GLint viewport[4]; + + glGetDoublev(GL_MODELVIEW_MATRIX, modelview); + glGetDoublev(GL_PROJECTION_MATRIX, projection); + glGetIntegerv(GL_VIEWPORT, viewport); + + hlMatrixMode(HL_TOUCHWORKSPACE); + hlLoadIdentity(); + + // Fit haptic workspace to view volume. + hluFitWorkspace(projection); + + // Compute cursor scale. + gManager->gCursorScale = hluScreenToModelScale(modelview, projection, viewport); + gManager->gCursorScale *= CURSOR_SIZE_PIXELS; + + hduVector3Dd p0, p1; + bool bNoError; + + bNoError = fromScreen(hduVector3Dd(0, 0, 0), p0); + assert(bNoError); + + bNoError = fromScreen(hduVector3Dd(1, 1, 0), p1); + assert(bNoError); + + double m_windowTworldScale = (p1 - p0).magnitude() / sqrt(2.0); + gManager->gWorldScale = m_windowTworldScale; +} +/******************************************************************************* + GLUT callback for idle state. Use this as an opportunity to request a redraw. + Checks for HLAPI errors that have occurred since the last idle check. +*******************************************************************************/ +void idleCallback(){ + HLerror error; + + while (HL_ERROR(error = hlGetError())){ + std::cerr << "HL Error: " << error.errorCode << std::endl <<error.errorInfo << std::endl; + + if (error.errorCode == HL_DEVICE_ERROR){ + hduPrintError(stderr, &error.errorInfo, "Device error\n"); + std::cout << "sending error message to haptic listener" << std::endl; + hapticCommandCB('e',0,0,0,0,0); + } + } + glutPostRedisplay(); +} + +/******************************************************************************* + This handler is called when the application is exiting. Deallocates any state + and cleans up. +*******************************************************************************/ +void exitProcedure(){ + + // Free up the haptic rendering context. + hlMakeCurrent(NULL); + + if (HapticManager::ghHLRC != NULL){ + hlDeleteContext(HapticManager::ghHLRC); + } + + // Free up the haptic device. + if (HapticManager::ghHD != HD_INVALID_HANDLE){ + hdDisableDevice(HapticManager::ghHD); + } + std::cout << "freeing haptic resources" << std::endl; +} + +/* initialize all the variable needed for the jni access to the Haptics class from the openGL thread*/ +void initJniVariables(void){ + /* --- CLASSES --- */ + //this class + hapticClass = env->GetObjectClass(*lock); + if(hapticClass == NULL){ + stopExecution("Could not find the Haptics class"); + } + // the haptic listener, member of this class + hapticListenerClass = env->FindClass("Luk/ac/qmul/eecs/ccmi/haptics/HapticListener;"); + if(hapticListenerClass == NULL){ + stopExecution("Could not find the haptic listener class"); + } + + /* --- FIELD IDS --- */ + // boolean set by the java thread when an element is added and it needs a new id from the haptic library + newHapticIdfieldId = env->GetFieldID(hapticClass,"newHapticId", "Z"); + if(newHapticIdfieldId == NULL){ + stopExecution("failed to find the newHapticId field id"); + } + + // boolean set by the java thread when an element is added and it needs a new id from the haptic library + dumpHapticIdfieldId = env->GetFieldID(hapticClass,"dumpHapticId", "Z"); + if(dumpHapticIdfieldId == NULL){ + stopExecution("failed to find the dumpHapticId field id"); + } + + // boolean set to this thread to notify the java thread the unsuccessful initialization of haptic device + hapticInitFailedfieldId = env->GetFieldID(hapticClass,"hapticInitFailed", "Z"); + if(hapticInitFailedfieldId == NULL){ + stopExecution("failed to find the hapticInitFailedfieldId field id"); + } + + // boolean set by the java thread to notify this thread the program has been shut down + shutdownfieldId = env->GetFieldID(hapticClass, "shutdown", "Z"); + if(shutdownfieldId == NULL){ + stopExecution("failed to find the shutdownfieldId field id"); + } + + // boolean set by the java thread when the user asks to sna + attractTofieldId = env->GetFieldID(hapticClass, "attractTo" , "Z"); + if(shutdownfieldId == NULL){ + stopExecution("failed to find the attractTo field id"); + } + + attractToHapticIdFieldId = env->GetFieldID(hapticClass, "attractToHapticId", "I"); + if(attractToHapticIdFieldId == NULL){ + stopExecution("failed to find the attractToHapticId field id"); + } + + pickUpfieldId = env->GetFieldID(hapticClass,"pickUp","Z"); + if(pickUpfieldId == NULL){ + stopExecution("failed to find the pickUp field id"); + } + + pickUpHapticIdFieldId = env->GetFieldID(hapticClass,"pickUpHapticId","I"); + if(pickUpHapticIdFieldId == NULL){ + stopExecution("failed to find pickUpHapticId field id"); + } + + hapticListenerFieldId = env->GetFieldID(hapticClass, "hapticListener", "Luk/ac/qmul/eecs/ccmi/haptics/HapticListener;"); + if(hapticListenerFieldId == NULL){ + stopExecution("failed to find the hapticListener field id"); + } + + // variable to exchange values between threads + currentHapticIdfieldId = env->GetFieldID(hapticClass,"currentHapticId","I"); + if(currentHapticIdfieldId == NULL){ + stopExecution("failed to find the currentHapticId field"); + } + + cmdFieldId = env->GetFieldID(hapticListenerClass,"cmd", "C"); + if(cmdFieldId == NULL){ + stopExecution("failed to find the cmd field id of the hapticListener class"); + } + + diagramElementFieldId = env->GetFieldID(hapticListenerClass, "diagramElementID", "I"); + if(diagramElementFieldId == NULL){ + stopExecution("failed to find the diagramElement field id of the hapticListener class"); + } + + xFieldId = env->GetFieldID(hapticListenerClass, "x", "D"); + if(xFieldId == NULL){ + stopExecution("failed to find the x field id of the hapticListener class"); + } + + yFieldId = env->GetFieldID(hapticListenerClass, "y", "D"); + if(yFieldId == NULL){ + stopExecution("failed to find the y field id of the hapticListener class"); + } + + startXFieldId = env->GetFieldID(hapticListenerClass, "startX", "D"); + if(startXFieldId == NULL){ + stopExecution("failed to find the x field id of the hapticListener class"); + } + + startYFieldId = env->GetFieldID(hapticListenerClass, "startY", "D"); + if(startYFieldId == NULL){ + stopExecution("failed to find the y field id of the hapticListener class"); + } + + hapticListener = env->GetObjectField(*lock,hapticListenerFieldId); + /* --- METHOD IDs --- */ + // notify method + notifyMethodId = env->GetMethodID(hapticClass,"notify","()V"); + if(notifyMethodId == NULL){ + stopExecution("failed to find the notify method id"); + } + + notifyListenerMethodId = env->GetMethodID(hapticListenerClass,"notify","()V"); + if(notifyListenerMethodId == NULL){ + stopExecution("failed to find the notify method id"); + } + + waitMethodId = env->GetMethodID(hapticListenerClass,"wait","()V"); + if(waitMethodId == NULL){ + stopExecution("failed to find the wait method id"); + } +} + +void hapticCommandCB(const jchar cmd, const jint ID, const jdouble x, const jdouble y, const jdouble startX, const jdouble startY){ + /* the haptic listener java thread is waiting for commands + first set the variable, the Haptic Listener java thread will read after being notified, + then notify and get it awake. Thus wait for the java thread to notify that the command + has been accomplished. This is done as otherwise some commands might be neglected. as if the thread + scheduler decides to execute twice this routine without executing the java thread in the middle then + the former command gets overwritten by the latter. + Note the monitor is hapticListener and not haptics as for the draw function. + When in this routine, this thread does not hold the lock on haptics as if a command results in changing + the elements collections (e.g. moveNode) all those methods are synchronized and require to acquire the lock on haptics. + Since this thread would wait for the command to be executed by the java thread, which in turns would wait for this + thread to release the lock on haptics, that would result in a deadlock. + */ + /* now wake up the haptic listener */ + if(env->MonitorEnter(hapticListener) != JNI_OK){ + stopExecution("Could not allocate memory for haptic listener thread monitor"); + } + checkExceptions(env,"Could not enter monitor on the haptic listener"); + env->SetCharField(hapticListener,cmdFieldId,cmd); + env->SetIntField(hapticListener,diagramElementFieldId,ID); + env->SetDoubleField(hapticListener,xFieldId,x); + env->SetDoubleField(hapticListener,yFieldId,y); + env->SetDoubleField(hapticListener,startXFieldId,startX); + env->SetDoubleField(hapticListener,startYFieldId,startY); + // wake the java thread up to execute the command + env->CallVoidMethod(hapticListener,notifyListenerMethodId); + checkExceptions(env, "Could not call notify() on HapticListener"); + /* wait for the commands to be executed. Here is actually where the monitor is + * freed and the java thread starts to execute the command, having been notified */ + env->CallVoidMethod(hapticListener,waitMethodId); + checkExceptions(env, "Could not call wait() on HapticListener"); + if(env->MonitorExit(hapticListener) != JNI_OK){ + stopExecution("Could not release memory for haptic listener thread monitor"); + } +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/uk_ac_qmul_eecs_ccmi_haptics_Haptics.h Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,27 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include <jni.h> +/* Header for class uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics */ + +#ifndef _Included_uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics +#define _Included_uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics +#ifdef __cplusplus +extern "C" { +#endif +#undef uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics_MIN_PRIORITY +#define uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics_MIN_PRIORITY 1L +#undef uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics_NORM_PRIORITY +#define uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics_NORM_PRIORITY 5L +#undef uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics_MAX_PRIORITY +#define uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics_MAX_PRIORITY 10L +/* + * Class: uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics + * Method: init + * Signature: (II)I + */ +JNIEXPORT jint JNICALL Java_uk_ac_qmul_eecs_ccmi_haptics_OmniHaptics_init + (JNIEnv *, jobject, jint, jint); + +#ifdef __cplusplus +} +#endif +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/utils.cpp Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,73 @@ +#include "utils.h" + +void checkExceptions(JNIEnv *env, char* what){ + if(env->ExceptionOccurred()){ + std::cout << "Exception occurred!!!" << std::endl; + std::cout << what << std::endl; + env->ExceptionDescribe(); + exit(-1); + } +} + +void stopExecution(char* msg){ + std::cerr << msg << std::endl; + exit(-1); +} + + +bool fromScreen(const hduVector3Dd &win, hduVector3Dd &obj) { + + hduMatrix m_worldTview; + hduMatrix m_viewTclip; + int m_viewport[4]; + glGetDoublev(GL_MODELVIEW_MATRIX, m_worldTview); + glGetDoublev(GL_PROJECTION_MATRIX, m_viewTclip); + glGetIntegerv(GL_VIEWPORT, m_viewport); + int nResult = gluUnProject( + win[0], win[1], win[2], + m_worldTview, + m_viewTclip, + m_viewport, + &obj[0], &obj[1], &obj[2]); + + return nResult == GL_TRUE; +} + +bool toScreen(const hduVector3Dd &obj, hduVector3Dd &win) { + + hduMatrix m_worldTview; + hduMatrix m_viewTclip; + int m_viewport[4]; + glGetDoublev(GL_MODELVIEW_MATRIX, m_worldTview); + glGetDoublev(GL_PROJECTION_MATRIX, m_viewTclip); + glGetIntegerv(GL_VIEWPORT, m_viewport); + int nResult = gluProject( + obj[0], obj[1], obj[2], + m_worldTview, + m_viewTclip, + m_viewport, + &win[0], &win[1], &win[2]); + + return nResult == GL_TRUE; +} + + +HDdouble norm(HDdouble v[3]){ + return sqrt( (v[0]*v[0])+(v[1]*v[1])+(v[2]*v[2])); +} + +void print(char* str){ + std::cout << str << std::endl; +} + +void print(int str){ + std::cout << str << std::endl; +} + +float rotate(){ + static float rot = 0.0f; + rot += 0.5f; + if(rot > 360) + rot = 0; + return rot; +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/native/PhantomOmni/utils.h Wed Apr 25 17:09:09 2012 +0100 @@ -0,0 +1,15 @@ +#pragma once + +#include <jni.h> +#include "stdafx.h" + +void checkExceptions(JNIEnv *env, char* what); +void stopExecution(char* msg); +bool fromScreen(const hduVector3Dd &win, hduVector3Dd &obj); +bool toScreen(const hduVector3Dd &obj, hduVector3Dd &win); +HDdouble norm(HDdouble v[3]); + +//TO REMOVE +void print(char* str); +float rotate(); +void print(int str);
--- a/native/WinNarrator/WinNarrator.vcproj Mon Feb 06 12:54:06 2012 +0000 +++ b/native/WinNarrator/WinNarrator.vcproj Wed Apr 25 17:09:09 2012 +0100 @@ -18,7 +18,7 @@ <Configurations> <Configuration Name="Debug|Win32" - OutputDirectory="..\workspace\ccmi" + OutputDirectory="$(SolutionDir)$(ConfigurationName)" IntermediateDirectory="$(ConfigurationName)" ConfigurationType="2" CharacterSet="1"