# HG changeset patch # User Fiore Martin # Date 1335370149 -3600 # Node ID 9e67171477bce9eeba6415ff4261ae29f6b93128 # Parent 4b2f975e35fa97091e32bddf3ecfb59e48ca9cb1 PHANTOM Omni Heptic device release diff -r 4b2f975e35fa -r 9e67171477bc README.txt --- /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®. + diff -r 4b2f975e35fa -r 9e67171477bc java/.classpath --- 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 @@ + + + + + + + diff -r 4b2f975e35fa -r 9e67171477bc java/libs/cmu_time_awb.jar Binary file java/libs/cmu_time_awb.jar has changed diff -r 4b2f975e35fa -r 9e67171477bc java/libs/cmu_us_kal.jar Binary file java/libs/cmu_us_kal.jar has changed diff -r 4b2f975e35fa -r 9e67171477bc java/libs/cmudict04.jar Binary file java/libs/cmudict04.jar has changed diff -r 4b2f975e35fa -r 9e67171477bc java/libs/cmulex.jar Binary file java/libs/cmulex.jar has changed diff -r 4b2f975e35fa -r 9e67171477bc java/libs/cmutimelex.jar Binary file java/libs/cmutimelex.jar has changed diff -r 4b2f975e35fa -r 9e67171477bc java/libs/en_us.jar Binary file java/libs/en_us.jar has changed diff -r 4b2f975e35fa -r 9e67171477bc java/libs/freetts.jar Binary file java/libs/freetts.jar has changed diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTree.java --- /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 . +*/ +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. + * + *
+ * {@code
+ * 	
+ *  
+ *   
+ *   
+ *   
+ *     
+ *   
+ *  
+ * }
+ * 
+ * + * @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 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; +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTreeCellRenderer.java --- /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 . +*/ +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; +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTreeNode.java --- /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 . +*/ +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. +*/ +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}. 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 { + public SetProperties() { + delegate = Collections.synchronizedSet(new HashSet()); + } + + public SetProperties(Collection collection) { + delegate = Collections.synchronizedSet(new HashSet(collection)); + } + + public SetProperties(int initialCapacity) { + delegate = Collections.synchronizedSet(new HashSet(initialCapacity)); + } + + public SetProperties(int initialCapacity, float loadFactor){ + delegate = Collections.synchronizedSet(new HashSet(initialCapacity, loadFactor)); + } + + /* DELEGATE METHODS */ + @Override + public boolean add(String arg0) { + return delegate.add(arg0); + } + + @Override + public boolean addAll(Collection 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 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[] 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 delegate; + private static String COMMENTS_ESCAPE = "#"; +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/XMLHandler.java --- /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 . +*/ +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() { + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(int i=0; i 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"; + + +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionListener.java --- 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 : + *
    + *
  • {@code name} : when the name of an element is changed + *
  • {@code properties} : when the all the properties of a node are changed all at once + *
  • {@code properties.clear} : when the all the properties of a node are deleted all at once + *
  • {@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 + *
  • {@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 + *
  • {@code property.remove} : when a property is removed from a node. + *
  • {@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. + *
  • {@code arrowHead} : when the arrow head of an edge is changed. + *
  • {@code endLabel} : + *
+ * @param e an object representing the change event + */ void elementChanged(ElementChangedEvent e); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionModel.java --- 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 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 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 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(); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramEdge.java --- 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 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; diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramElement.java --- 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){ + void setNotifier(DiagramModel.ReentrantLockNotifier notifier){ this.notifier = notifier; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModel.java --- 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 . -*/ + */ 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(INITIAL_NODES_SIZE); edges = new ArrayList(INITIAL_EDGES_SIZE); elements = new ArrayList(INITIAL_NODES_SIZE+INITIAL_EDGES_SIZE); - + changeListeners = new LinkedList(); - - 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 getDiagramCollection(){ return diagramCollection; } - + /** * Returns a TreeModel for this diagram * @@ -116,58 +110,64 @@ public TreeModel 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 { public InnerDiagramCollection(){ listeners = new ArrayList(); } - + @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 getEdges() { return Collections.unmodifiableCollection(edges); } - + @Override public Collection 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 listeners; } - - @SuppressWarnings("serial") + + @SuppressWarnings("serial") private class InnerTreeModel extends DefaultTreeModel implements TreeModel{ - public InnerTreeModel(TreeNode root){ - super(root); - bookmarks = new LinkedHashMap(); - } - - @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(); + diagramTreeNodeListeners = new ArrayList(); } @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 getBookmarks(){ return new LinkedHashSet(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 bookmarks; - } - - private synchronized boolean _insert(N n, Object source) { - assert(n != null); + private Map bookmarks; + private ArrayList 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 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 empty = Collections.emptyList(); - for(int i=0; i empty = Collections.emptyList(); + for(int i=0; i p = (Pair)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 p = (Pair)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 p = (Pair)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 p = (Pair)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 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 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 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 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 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 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 nodes; private ArrayList edges; private ArrayList elements; private InnerTreeModel treeModel; - + private long edgeCounter; private long nodeCounter; - - private ElementNotifier notifier; + + private ReentrantLockNotifier notifier; private List 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;} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModelTreeNode.java --- 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 . -*/ -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(); - } - - 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 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 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(); - 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(); - } - } -} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramNode.java --- 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(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(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(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 modifierIndexes){ + public void setModifierIndexes(String propertyType, int propertyValueIndex, Set modifierIndexes,Object source){ + StringBuilder oldIndexes = new StringBuilder(); + List modifierTypes = getProperties().getModifiers(propertyType).getTypes(); + Set 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(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 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); diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramTreeNode.java --- /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 . +*/ +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(); + } + + /** + * 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 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 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(); + 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(); + } + } +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramTreeNodeEvent.java --- /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 . + */ +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; +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramTreeNodeListener.java --- /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 . + */ +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); +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceHolderMutableTreeNode.java --- 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceMutableTreeNode.java --- 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; diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ElementChangedEvent.java --- 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; + } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/MalformedEdgeException.java --- 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 . +*/ /* 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); - } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeProperties.java --- 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 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 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); diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeReferenceMutableTreeNode.java --- 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(){ diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyMutableTreeNode.java --- 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyTypeMutableTreeNode.java --- 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; - } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TreeModel.java --- 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(); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TypeMutableTreeNode.java --- 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/AudioFeedback.java --- /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 . +*/ +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. +*/ +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 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 workList = new ArrayList(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(){ + @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; + } + }); + } + + ListalreadyLockedElements = new ArrayList(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 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 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 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 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 selectedElements; + private static ResourceBundle resources = ResourceBundle.getBundle(CCmIPopupMenu.class.getName()); +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/CCmIPopupMenu.properties --- /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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/Diagram.java --- 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 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 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 modifiers) { - node.setModifierIndexes(type, index, modifiers); + Set 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); } } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramEventSource.java --- /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 . +*/ +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); + +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramModelUpdater.java --- 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 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 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); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramPanel.java --- 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 . -*/ + */ 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); +} + diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramTree.java --- 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. + *

+ * 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(); @@ -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)super.getModel(); } + /** + * @see javax.swing.JTree#setModel(javax.swing.tree.TreeModel) + * + * @param newModel the new mnodel for this tree + */ public void setModel(TreeModel 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 tempList = new ArrayList(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= 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 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); } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/Direction.java --- 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; diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java --- 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 . -*/ + */ 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(); } - + + /* --- 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 nodes) throws ConnectNodesException{ + assert(getNodesNum() == 0); + /* this is to eliminate duplicates */ + LinkedHashSet nodeSet = new LinkedHashSet(); + 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(); + 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 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 getConnectionPoints(){ - List list = new LinkedList(); - 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 nodes) throws ConnectNodesException{ - assert(getNodesNum() == 0); - /* this is to eliminate duplicates */ - LinkedHashSet nodeSet = new LinkedHashSet(); - 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(); - 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 pItr = points.listIterator(); pItr.hasNext() && !found; ){ - InnerPoint ePoint = pItr.next(); - for(ListIterator 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 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 getConnectionPoints(){ + List list = new LinkedList(); + 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 pItr = points.listIterator(); pItr.hasNext() && !found; ){ + InnerPoint ePoint = pItr.next(); + for(ListIterator 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 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 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 attachedNodes = new ArrayList(nodeList.getLength()); - List labels = new ArrayList(nodeList.getLength()); - for(int i=0; i 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 attachedNodes = new ArrayList(nodeList.getLength()); + List labels = new ArrayList(nodeList.getLength()); + for(int i=0; i pointsId = new LinkedHashMap(); NodeList pointTagList = edgeTag.getElementsByTagName(PersistenceManager.POINT); - + for(int i=0; i nodes; + + /* list containing the vertex of the edge which are not nodes */ + /** + * The list of the inner points of this edge + */ + protected List 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 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(); } - + @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 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 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. -*/ -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 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. -*/ + */ 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(); - 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(10); - existingTemplates = new ArrayList(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(); + 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(10); + existingTemplates = new ArrayList(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 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 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 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 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 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= 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= 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 existingTemplateNames; - private ArrayList 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 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 existingTemplateNames; + private ArrayList 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 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); + } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.properties --- 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/EditorTabbedPane.java --- 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. +*/ 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; + } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/Finder.java --- 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 iterationSet = new HashSet(selectedElements); - HashSetalreadyLockedElements = new HashSet(); - /* 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 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 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 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; diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/HapticKindle.java --- 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 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 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 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 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 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 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 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/HapticTrigger.java --- 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() ); } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/LineStyle.java --- 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/Node.java --- 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); } } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/NodePopupMenu.java --- 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 . -*/ - -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()); -} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/NodePopupMenu.properties --- 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/PropertyEditorDialog.java --- 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)); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechLogDialog.java --- 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 . -*/ - -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; -} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechMenuFactory.java --- 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()); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechOptionPane.java --- 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 int showProgressDialog(Component parentComponent, String message,final ProgressDialogWorker 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 extends SwingWorker { + 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/TemplateEditor.java --- 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 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 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(); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessFilter.java --- /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 . +*/ +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; + +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessFilter.properties --- /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} + diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessPanel.java --- /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 . +*/ +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; +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessPanelEditor.java --- /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 . +*/ +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()); + } + + 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 awarenessPanels; + private static int TIMER_DELAY = 2000; +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessTextPane.java --- /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 . +*/ + +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(); + 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 records; +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/BroadcastFilter.java --- /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 . +*/ +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; + +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/BroadcastFilterTree.xml --- /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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/DisplayFilter.java --- /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 . +*/ +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; +} + + diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/DisplayFilterTree.xml --- /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 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileSystemTree.java --- 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 filePath = new LinkedList(); 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.java --- 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 { diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.properties --- 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: diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PersistenceManager.java --- 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 enumeration = treeRoot.depthFirstEnumeration(); enumeration.hasMoreElements();){ - DiagramModelTreeNode treeNode = enumeration.nextElement(); + Enumeration 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 collectionModel = diagram.getCollectionModel(); TreeModel treeModel = diagram.getTreeModel(); + /* a map linking node ids in the XML file to the actual Node object they represent */ Map nodesId = new LinkedHashMap(); 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 model, String path) throws IOException{ - DiagramModelTreeNode treeNode = (DiagramModelTreeNode)model.getRoot(); + private static DiagramTreeNode getTreeNodeFromString(TreeModel 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); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/haptics/DummyHaptics.java --- 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(){ diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListenerCommand.java --- 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/haptics/Haptics.dll Binary file java/src/uk/ac/qmul/eecs/ccmi/haptics/Haptics.dll has changed diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/haptics/Haptics.java --- 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(); - + } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticsFactory.java --- 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(){ diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/haptics/Node.java --- 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 edges; - } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/haptics/OmniHaptics.java --- /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 . +*/ + +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>(); + edges = new HashMap>(); + currentNodes = EMPTY_NODE_LIST; + currentEdges = EMPTY_EDGE_LIST; + nodesMaps = new HashMap>(); + edgesMaps = new HashMap>(); + 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 cNodes = new ArrayList(30); + ArrayList cEdges = new ArrayList(30); + nodes.put(name, cNodes); + edges.put(name, cEdges); + + HashMap cNodesMap = new HashMap(); + HashMap cEdgesMap = new HashMap(); + 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 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 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 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 currentNodes; + private ArrayList currentEdges; + private HashMap currentNodesMap; + private HashMap currentEdgesMap; + + /* maps with all the diagrams in the editor */ + private HashMap> nodes; + private HashMap> edges; + private HashMap> nodesMaps; + private HashMap> 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 EMPTY_NODE_LIST = new ArrayList(0); + private static final ArrayList EMPTY_EDGE_LIST = new ArrayList(0); + private static final HashMap EMPTY_NODE_MAP = new HashMap(); + private static final HashMap EMPTY_EDGE_MAP = new HashMap(); + +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.java --- 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; diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.properties --- 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/main/Organization Chart.xml --- /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 @@ + + + + + Entity + Rectangle + + + Responsibility + + + + + + Connection + Solid + 2 + 2 + + + diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/main/Tube.xml --- /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 @@ + + + + + Stations + Circle + + + + Central Line + Solid + 2 + 2 + + + Victoria Line + Solid + 2 + 2 + + + Jubilee Line + Solid + 2 + 2 + + + Piccadilly Line + Solid + 2 + 2 + + + Circle Line + Solid + 2 + 2 + + + diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/main/UML Diagram.xml --- /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 @@ + + + + + Class + Rectangle + + + attributes + + + + static + + + + protected + + + + private + + + + + + operations + + + + static + + + + protected + + + + private + + + + + + + + Association + Solid + 2 + 2 + + + + + + + Generalization + Dashed + 2 + 2 + + + + + + + diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/AwarenessMessage.java --- /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 . +*/ +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 ; + } + } +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/CCmIOSCPacketCodec.java --- 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); }*/ } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/ClientConnectionManager.java --- 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(); requests = new ConcurrentLinkedQueue(); answers = new LinkedBlockingQueue(); @@ -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 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 nodesToConnect = new ArrayList(cmd.getArgNum()-1); - for(int i=1;i(cmd.getArgNum()-3); - for(int i=3;i(cmd.getArgNum()-3); + for(int i=3;i(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> entryset = channels.entrySet(); + SocketChannel channel = null; for(Map.Entry 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 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/Command.java --- 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; } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/CommandExecutor.java --- 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 m, DiagramElement de){ + public Insert(CollectionModel 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 nodesToConnect = new ArrayList(nodes.length); + /* retrieve the nodes to connect by the id, conveyed in the message */ + for(int i = 0; i model; private DiagramElement element; + private long[] nodes; } public static class Remove extends CommandExecutor{ - public Remove(CollectionModel m, DiagramElement de) { + public Remove(CollectionModel 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 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 m, DiagramModelTreeNode tn, String n){ + public SetNotes(TreeModel 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 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 i){ + public SetModifiers(Node n,String t,Integer v, Set 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/DiagramDownloader.java --- 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 { - 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/DiagramEventActionSource.java --- /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 . +*/ +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 ""; + } + }; + +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/LockMessage.java --- 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; } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/Message.java --- 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(); diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/NetDiagram.java --- 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 modifiers) { + Set 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 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 diagramElements; private Protocol protocol; private ExceptionHandler exceptionHandler; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/NetworkThread.java --- /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 . +*/ +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; +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/OscProtocol.java --- 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",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(); - 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); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/Server.properties --- 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/ServerConnectionManager.java --- 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 diagrams){ + ServerConnectionManager( Map diagrams, AwarenessPanelEditor panelEditor) { this.diagrams = diagrams; - diagramChannelAllocations = new HashMap>(); + awarenessPanelEditor = panelEditor; + diagramChannelAllocations = new HashMap>(); localhostDiagramElementQueue = new ConcurrentHashMap>(); 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> entry : diagramChannelAllocations.entrySet()){ - if(entry.getValue().remove(channel)){ + /* looks for the Set containing this channel */ + for(Map.Entry> 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 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 list = new HashSet(); - list.add(localChannel); + Set list = new HashSet(); + 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 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 nodesToConnect = new ArrayList(cmd.getArgNum()-1); - /* retrieve the nodes to connect by the id, conveyed in the message */ - for(int i = 1; i(cmd.getArgNum()-3); - for(int i=3;i(cmd.getArgNum()-3); + for(int i=3;i> 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 "newNameoldName" * + * 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 "newNameoldName" 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> 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 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 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 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> diagramChannelAllocations; + private Map> 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> 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(); + } + + SocketChannel channel; + String userName; + List lockAwarenessSources; + } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/network/ServerLockManager.java --- 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 locks, SocketChannel channel){ + private boolean lockExists(DiagramTreeNode treeNode, Lock lock, List 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 locks, SocketChannel channel){ + private boolean lockExists(DiagramTreeNode treeNode, Lock lock1, Lock lock2, List 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 locks){ + private boolean checkLockDependencies(DiagramTreeNode treeNode, Lock lock, SocketChannel channel, List 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 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 locks = locksMap.get(diagramName); Iterator 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> 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; } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/EdgeDrawSupport.java --- 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("" + s + ""); diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeEdge.java --- 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() ); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardDialog.java --- 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); } }); diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/sound/BeadsSound.java --- 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/sound/Sound.java --- 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(); } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/speech/BeadsAudioPlayer.java --- /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 . +*/ + +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; + +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/speech/DummyNarrator.java --- 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.java --- 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; + } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.properties --- 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 diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/speech/NativeNarrator.java --- 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(); 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 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; } } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/speech/SpeechUtilities.java --- 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) { diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/speech/TreeSonifier.java --- /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 . +*/ +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)); + } + +} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/speech/WinNarrator.dll Binary file java/src/uk/ac/qmul/eecs/ccmi/speech/WinNarrator.dll has changed diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/utils/GridBagUtilities.java --- 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; } diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/utils/InteractionLog.java --- 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); diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/utils/NativeLibFileWriter.java --- 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 . -*/ - -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; -} diff -r 4b2f975e35fa -r 9e67171477bc java/src/uk/ac/qmul/eecs/ccmi/utils/ResourceFileWriter.java --- /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 . +*/ + +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; +} diff -r 4b2f975e35fa -r 9e67171477bc native/PhantomOmni/CollectionsManager.cpp --- /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.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.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.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.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(&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(&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; iReleaseDoubleArrayElements(*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(&adjMatrixAsObj); + + for(unsigned int i=0; iGetObjectArrayElement(*jadjMatrix,i); + if(adjMatrixBitSet == NULL) + stopExecution("Cannot get the adjMatrix field array element"); + for(unsigned int j=0;jCallBooleanMethod(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 diff -r 4b2f975e35fa -r 9e67171477bc native/PhantomOmni/CollectionsManager.h --- /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 +#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