Mercurial > hg > accesspd
changeset 0:78b7fc5391a2
first import, outcome of NIME 2014 hackaton
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,15 @@ +syntax: glob +re:^java/bin/uk/ac/qmul/eecs/ccmi/checkboxtree/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/diagrammodel/ +*.class +re:^java/bin/uk/ac/qmul/eecs/ccmi/gui/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/haptics/HAPI/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/haptics/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/main/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/network/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/pdsupport/audio/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/simpletemplate/audio/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/sound/audio/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/speech/ +re:^java/bin/uk/ac/qmul/eecs/ccmi/simpletemplate/ +re:^java/bin/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/.classpath Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/> + <classpathentry kind="lib" path="libs/beads.jar"/> + <classpathentry kind="lib" path="libs/cmu_time_awb.jar"/> + <classpathentry kind="lib" path="libs/cmu_us_kal.jar"/> + <classpathentry kind="lib" path="libs/cmudict04.jar"/> + <classpathentry kind="lib" path="libs/cmulex.jar"/> + <classpathentry kind="lib" path="libs/cmutimelex.jar"/> + <classpathentry kind="lib" path="libs/en_us.jar"/> + <classpathentry kind="lib" path="libs/freetts.jar"/> + <classpathentry kind="lib" path="libs/jl1.0.1.jar"/> + <classpathentry kind="lib" path="libs/JWizardComponent.jar"/> + <classpathentry kind="lib" path="libs/mp3spi1.9.4.jar"/> + <classpathentry kind="lib" path="libs/NetUtil.jar"/> + <classpathentry kind="lib" path="libs/tritonus_share.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/.project Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>accessPD</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/.settings/org.eclipse.jdt.core.prefs Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,12 @@ +#Fri Dec 16 13:28:00 GMT 2011 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTree.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,158 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.checkboxtree; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.SAXException; + + +/** + * A JTree containing {@code CheckBoxTreeNode} nodes. The tree is built according to an XML file + * passed as argument to the constructor. + * The XML file hierarchical structure reflects the structure of the tree. XML tag name can be + * either {@code selectable} or {@code unselectable}. The former representing tree nodes with a check + * box associated to it, and the latter a normal tree node, much as a {@link DefaultMutableTreeNode}. + * Either tags must have an attribute {@code value}, representing the name of the node which will be displayed + * in the tree. Here is an example of a simple XML file, representing a tree with the tree root (non selectable) having + * three children, the first of which has, in turn, a child. all the descendants of the root are selectable, but the + * first child. + * + * <pre> + * {@code + * <?xml version="1.0" encoding="utf-8"?> + * <unselectable value="root"> + * <unselectable value="first child"/> + * <selectable value="second child"/> + * <selectable value "third child"> + * <selectable value="grand child"/> + * </selectable> + * </unselectable> + * } + * </pre> + * + * @see CheckBoxTreeNode + */ +@SuppressWarnings("serial") +public class CheckBoxTree extends JTree { + public CheckBoxTree(InputStream stream, SetProperties values){ + super(new DefaultTreeModel(new DefaultMutableTreeNode())); + this.properties = values; + getAccessibleContext().setAccessibleName("tree"); + treeModel = (DefaultTreeModel)getModel(); + setCellRenderer(new CheckBoxTreeCellRenderer()); + buildTree(stream); + /* mouse listener to toggle the selected tree node */ + addMouseListener(new MouseAdapter(){ + @Override + public void mousePressed(MouseEvent e){ + TreePath path = getPathForLocation(e.getX(),e.getY()); + if(path == null) + return; + CheckBoxTreeNode treeNode = (CheckBoxTreeNode)path.getLastPathComponent(); + toggleSelection(treeNode); + } + }); + } + + /** + * Builds a CheckBoxTree out of an xml file passed as argument. All the nodes + * of the tree are marked unchecked. + * + * @param stream an input stream to an xml file + */ + public CheckBoxTree(InputStream stream){ + this(stream,null); + } + + /* use a sax parser to build the tree, if an error occurs during the parsing it just stops */ + private void buildTree(InputStream stream){ + SAXParserFactory factory = SAXParserFactory.newInstance(); + try { + SAXParser saxParser = factory.newSAXParser(); + XMLHandler handler = new XMLHandler((DefaultTreeModel)getModel(),properties); + saxParser.parse(stream, handler); + } catch (IOException e) { + e.printStackTrace(); + return; + } catch (ParserConfigurationException e) { + e.printStackTrace(); + return; + } catch (SAXException e) { + e.printStackTrace(); + return; + } + } + + /** + * Returns a reference to the properties holding which nodes of the tree are currently checked. + * @return the properties or {@code null} if the constructor with no properties was used. + */ + public SetProperties getProperties(){ + return properties; + } + + /** + * Toggle the check box of the tree node passed as argument. If the tree node + * is not a leaf, then all its descendants will get the new value it. That is, + * if the tree node becomes selected as a result of the call, then all the descendants + * will become in turn selected (regardless of their previous state). If it becomes + * unselected, the descendants will become unselected as well. + * + * @param treeNode the tree node to toggle + */ + public void toggleSelection(CheckBoxTreeNode treeNode){ + if(treeNode.isSelectable()){ + boolean selection = !treeNode.isSelected(); + treeNode.setSelected(selection); + if(selection) + properties.add(treeNode.getPathAsString()); + else + properties.remove(treeNode.getPathAsString()); + treeModel.nodeChanged(treeNode); + if(!treeNode.isLeaf()){ + for( @SuppressWarnings("unchecked") + Enumeration<CheckBoxTreeNode> enumeration = treeNode.depthFirstEnumeration(); enumeration.hasMoreElements();){ + CheckBoxTreeNode t = enumeration.nextElement(); + t.setSelected(selection); + if(selection) + properties.add(t.getPathAsString()); + else + properties.remove(t.getPathAsString()); + treeModel.nodeChanged(t); + } + } + } + } + + private SetProperties properties; + private DefaultTreeModel treeModel; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTreeCellRenderer.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,75 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.checkboxtree; + +import java.awt.Component; + +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellRenderer; + + +/** + * A tree cell renderer which renders {@code CheckBoxTreeNode} objects like a {@code JCheckBox}: + * thick box (showing whether the node is selected or not) followed by a label (same label it would + * appear with a default {@code DefaultTreeCellRenderer}. + * + */ +@SuppressWarnings("serial") +public class CheckBoxTreeCellRenderer extends DefaultTreeCellRenderer { + + public CheckBoxTreeCellRenderer(){ + checkBox = new JCheckBox(); + checkBox.setBorder(BorderFactory.createLineBorder(this.getBorderSelectionColor())); + } + /** + * Returns the {@code Component} that the renderer uses to draw the value + * + * @throws ClassCastException if {@code value} is not an instance of {@code CheckoxTreeNode} + * @see javax.swing.tree.TreeCellRenderer#getTreeCellRendererComponent(JTree, Object, boolean, boolean, boolean, int, boolean) + * @return the {@code Component} that the renderer uses to draw the value + */ + @Override + public Component getTreeCellRendererComponent(JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus){ + + CheckBoxTreeNode treeNode = (CheckBoxTreeNode)value; + if(!treeNode.isSelectable()) + return super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + + checkBox.setSelected(treeNode.isSelected()); + checkBox.setText(value.toString()); + if(selected){ + checkBox.setBackground(getBackgroundSelectionColor()); + checkBox.setBorderPainted(true); + }else{ + checkBox.setBackground(getBackgroundNonSelectionColor()); + checkBox.setBorderPainted(false); + } + return checkBox; + } + + private JCheckBox checkBox; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/CheckBoxTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,115 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.checkboxtree; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; + + +/** + * A special {@code DefaultMutableTreeNode} that has, in addition, the property of being (or not) + * selected. + * + */ +@SuppressWarnings("serial") +public class CheckBoxTreeNode extends DefaultMutableTreeNode { + + /** + * Construct a new instance of this class. + * + * @param userObject a user object for this tree node @see javax.swing.tree#DefaultMutableTreeNode + * @param selectable whether or not this node is selectable. A non selectable node is pretty much + * equivalent to a {@code DefaultMutableTreeNode}. + */ + public CheckBoxTreeNode(Object userObject, boolean selectable){ + super(userObject); + this.selectable = selectable; + selected = false; + } + + /** + * Returns {@code true} if the node is selected, or {@code false} otherwise. + * + * @return {@code true} if the node is selected, or {@code false} otherwise. + */ + public boolean isSelected() { + return selected; + } + + /** + * Makes the node selected or unselected + * + * @param selected {@code true} to select the node, {@code false} to unselect it. + */ + public void setSelected(boolean selected) { + if(selectable){ + this.selected = selected; + } + } + + /** + * Whether the node is selectable or not. This depends on the value of the + * {@code selected} parameter passed to the constructor. + * + * @return {@code true} if the node is selectable, {@code false} otherwise. + */ + public boolean isSelectable(){ + return selectable; + } + + /** + * Returns a string representation of the audio description of this node. + * The value returned by this method can be passed to a Text-To-Speech synthesizer + * for speech rendering. + * + * @return the audio description of this node. + */ + public String spokenText(){ + if(selectable) + return toString()+", "+ (selected ? "checked": "unchecked"); + else + return toString(); + } + + /** + * Returns a string representation of the path of this tree node. The the string is made up + * as the concatenation of the tree node names from the root to this node, separated by + * {@link #STRING_PATH_SEPARATOR} + * + * @return the node path as a String + */ + public String getPathAsString(){ + StringBuilder builder = new StringBuilder(); + TreeNode [] path = getPath(); + for(int i=0; i<path.length; i++){ + builder.append(path[i].toString()); + if(i != path.length-1) + builder.append(STRING_PATH_SEPARATOR); + } + return builder.toString(); + } + + private boolean selected; + private boolean selectable; + /** + * The character used as a separator when returning the string representation of the path + * of this node via {@link #getPathAsString()}. + */ + public static final char STRING_PATH_SEPARATOR = '.'; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/SetProperties.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,206 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.checkboxtree; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * The {@code SetProperties} class represents a persistent set of properties. + * The {@code SetProperties} can be saved to a stream or loaded from a stream. + * + * Unlike {@code java.util.Properties}, this class is not backed by a key-value map, but rather + * by a {@code Set<String>}. Therefore it only contains values. All the methods of this class are thread-safe, + * but {@code iterator()}. In order to safely iterate on the Set, the iteration must happen in a block syncronized + * on the Object returned by {@link #getMonitor()}. + * + * For a description of the methods of the {@code Set} interface, see {@code java.util.Set} + * + * @see java.util.Properties + * @see java.util.Collections#synchronizedSet(Set) + */ +public class SetProperties implements Set<String> { + public SetProperties() { + delegate = Collections.synchronizedSet(new HashSet<String>()); + } + + public SetProperties(Collection<? extends String> collection) { + delegate = Collections.synchronizedSet(new HashSet<String>(collection)); + } + + public SetProperties(int initialCapacity) { + delegate = Collections.synchronizedSet(new HashSet<String>(initialCapacity)); + } + + public SetProperties(int initialCapacity, float loadFactor){ + delegate = Collections.synchronizedSet(new HashSet<String>(initialCapacity, loadFactor)); + } + + /* DELEGATE METHODS */ + @Override + public boolean add(String arg0) { + return delegate.add(arg0); + } + + @Override + public boolean addAll(Collection<? extends String> arg0) { + return delegate.addAll(arg0); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public boolean contains(Object arg0) { + return delegate.contains(arg0); + } + + @Override + public boolean containsAll(Collection<?> arg0) { + return delegate.containsAll(arg0); + } + + @Override + public boolean equals(Object arg0) { + return delegate.equals(arg0); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Iterator<String> iterator() { + return delegate.iterator(); + } + + @Override + public boolean remove(Object arg0) { + return delegate.remove(arg0); + } + + @Override + public boolean removeAll(Collection<?> arg0) { + return delegate.removeAll(arg0); + } + + @Override + public boolean retainAll(Collection<?> arg0) { + return delegate.retainAll(arg0); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + @Override + public <T> T[] toArray(T[] arg0) { + return delegate.toArray(arg0); + } + + /** + * Stores the content of this set (strings) in a text file. The content can then be retrieved + * by calling {@code load()} passing as argument the same file as this method. The strings + * will be written on a row each. + * + * @param file A valid File where the content of this object will be stored + * @param comments A comment string that will be added at the beginning of the file. + * + * @throws IOException if an exception occurs while writing the file + */ + public void store(File file, String comments) throws IOException{ + synchronized(delegate){ + if(file == null) + throw new IllegalArgumentException("File cannot be null"); + FileWriter fWriter = new FileWriter(file); + BufferedWriter writer = new BufferedWriter(fWriter); + if(comments != null){ + writer.write(COMMENTS_ESCAPE+" "+comments); + writer.newLine(); + writer.newLine(); + } + + for(String property : this){ + writer.write(property); + writer.newLine(); + } + writer.close(); + } + } + + /** + * Loads the content of a file into this set. Whaen the file is read, each row is taken as an + * entry of the set. + * + * @param file the file where to read the entries from + * @throws IOException if an exception occurs while reading the file + */ + public void load(File file) throws IOException{ + synchronized(delegate){ + if(file == null) + throw new IllegalArgumentException("File cannot be null"); + FileReader fReader = new FileReader(file); + BufferedReader reader = new BufferedReader(fReader); + String line; + while((line = reader.readLine()) != null){ + if(!line.isEmpty() && !line.trim().startsWith(COMMENTS_ESCAPE)) + add(line); + } + reader.close(); + } + } + + /** + * Returns the Object all the methods (but {@code iterator()} of this {@code Set} are synchronized on. It can be used + * to safely iterate on this object without incurring in a race condition + * + * @return an {@code Object} to be used as synchronization monitor + * + * @see java.util.Collections#synchronizedSet(Set) + */ + public Object getMonitor(){ + return delegate; + } + + private Set<String> delegate; + private static String COMMENTS_ESCAPE = "#"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/checkboxtree/XMLHandler.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,106 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.checkboxtree; + +import java.util.Stack; + +import javax.swing.tree.DefaultTreeModel; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/* Build a tree out of an XML file. The hierarchical structure of the XML file reflects + * the structure of the tree. The XMl file must have the form specified in CheckBoxTree + * class comment. + * + */ +class XMLHandler extends DefaultHandler { + + @SuppressWarnings("serial") + public XMLHandler(DefaultTreeModel treeModel, SetProperties properties){ + this.properties = properties; + this.treeModel = treeModel; + /* path is used to keep track of the current node's path. If the path is present as a * + * properties entry, then the current node, that has jus been created, will be set as selected */ + path = new Stack<String>() { + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(int i=0; i<size();i++){ + builder.append(get(i)); + if(i != size()-1) + builder.append(CheckBoxTreeNode.STRING_PATH_SEPARATOR); + } + return builder.toString(); + } + }; + } + + /* + * Create a CheckBoxTreeNode out of an xml tag. The tree node name is given by the value + * attribute. Whether the tree node is selectable or not, depends on the tag name, which can be + * selectable/unselectable. If the node is selectable, then it will be set as selected if its path + * is present in the properties + */ + @Override + public void startElement(String uri, + String localName, + String qName, + Attributes attributes) + throws SAXException { + String nodeName = attributes.getValue(VALUE_ATTR); + if(nodeName == null) + throw new SAXException("Value attribute missing"); + boolean isSelectable = SELECTABLE_NODE.equals(qName); + + CheckBoxTreeNode newNode = new CheckBoxTreeNode(nodeName,isSelectable); + if(currentNode == null){ + currentNode = newNode; + treeModel.setRoot(newNode); + }else{ + currentNode.add(newNode); + } + currentNode = newNode; + path.push(nodeName); + if(properties.contains(path.toString())) + newNode.setSelected(true); + } + + /* when an end tag is encountered, we carry on building the tree with the father as current node */ + @Override + public void endElement(String uri, + String localName, + String qName) + throws SAXException { + path.pop(); + currentNode = (CheckBoxTreeNode)currentNode.getParent(); + } + + private CheckBoxTreeNode currentNode; + private DefaultTreeModel treeModel; + private SetProperties properties; + private Stack<String> path; + /* attributes used in the XML file */ + public static final String VALUE_ATTR = "value"; + public static final String SELECTABLE_NODE = "selectable"; + public static final String UNSELECTABLE_NODE = "selectable"; + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionEvent.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,52 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.EventObject; + +/** + * An object representing a collection event. Collection events happen when a Diagram + * Element is either inserted or removed from the DiagramModel via the CollectionModel. + * + */ +@SuppressWarnings("serial") +public class CollectionEvent extends EventObject { + + /** + * + * @param source the source of the event + * @param element the diagram element that has been added or removed from the collection + */ + public CollectionEvent(Object source, DiagramElement element) { + super(source); + this.element = element; + } + + /** + * + * @return the diagram element whose addition or removal from the collection + * triggered this event. + */ + public DiagramElement getDiagramElement(){ + return element; + } + + private DiagramElement element; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionListener.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,65 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * A listener to a collection event. A collection event is triggered whenever a diagram element + * is added or removed from the collection or when a diagram element contained in the collection is + * changed (e.g. when a new Name for the element is set) + * + */ +public interface CollectionListener { + + /** + * Called when either a node or an edge is inserted in the model. + * @param e an object representing the insertion event + */ + void elementInserted(CollectionEvent e); + + /** + * Called when either a node or an edge is removed in the model. + * @param e an object representing the remotion event + */ + void elementTakenOut(CollectionEvent e); + + /** + * Called when either a node or an edge in the model this listener is registered on. + * In order to identify which part of the element has been changed the call {@code e.getChangeType()} + * can be used. Furthermore the call {@code e.getArguments()} can return an object + * with additional information related to the change. + * The string returned by {@code e.getChangeType()} can be one of the following : + * <ul> + * <li>{@code name} : when the name of an element is changed + * <li>{@code properties} : when the all the properties of a node are changed all at once + * <li>{@code properties.clear} : when the all the properties of a node are deleted all at once + * <li>{@code property.add} : when a new property is added to a node. {@code e.getArguments()} will return + * a {@code PropertyChangeArgs} object, to retrieve the property from the node + * <li>{@code property.set} : when a property value changes into another value. {@code e.getArguments()} will return + * a {@code PropertyChangeArgs} object, to retrieve the property from the node + * <li>{@code property.remove} : when a property is removed from a node. + * <li>{@code property.modifiers} : when modifiers of a property are changed. {@code e.getArguments()} will return + * a {@code PropertyChangeArgs} object to retrieve the property from the node. {@code getOldValue()} will return in this + * case a String concatenation of all the modifier that were set before the change for this property. + * <li>{@code arrowHead} : when the arrow head of an edge is changed. + * <li>{@code endLabel} : when the label of an edge is changed. + * </ul> + * @param e an object representing the change event + */ + void elementChanged(ElementChangedEvent e); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/CollectionModel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,135 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.Collection; +import java.util.concurrent.locks.ReentrantLock; + +import javax.swing.event.ChangeListener; + +/** + * + * Represents the collection side of a DiagramModel instance. + * + * @param <N> a type extending DiagramNode + * @param <E> a type extending DiagramEdge + */ +public interface CollectionModel<N extends DiagramNode, E extends DiagramEdge> { + /** + * Adds a collection listener to the model. + * @param listener the listener to be added + */ + void addCollectionListener(CollectionListener listener); + /** + * Removed a collection listener to the model. + * @param listener the listener to be removed + */ + void removeCollectionListener (CollectionListener listener); + + /** + * insert a DiagramNode into the diagram model + * @param n the DiagramNode to be inserted in the collection + * @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, 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, 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, Object source); + + /** + * Returns the diagram nodes contained by the model as a collection + * @return the collection of diagram nodes + */ + Collection<N> getNodes(); + + /** + * Returns the diagram edges contained by the model as a collection + * @return the collection of diagram edges + */ + Collection<E> getEdges(); + + /** + * return a list of nodes and edges in the model as a unique collection + * of diagram elements. + * @return the collection of diagram elements + */ + Collection<DiagramElement> getElements(); + + /** + * Add a change listener to the model. the listeners will be fired each time the model + * goes from the unmodified to modified state. The model is modified when a either a + * node or an edge are inserted or removed or changed when they are within the model. + * @param l a ChangeListener to add to the model + */ + void addChangeListener(ChangeListener l); + + /** + * Removes a change listener from the model. + * @param l a ChangeListener to remove from the model + */ + void removeChangeListener(ChangeListener l); + + /** + * Returns true if the model has been modified + * @return true if the model has been modified + */ + boolean isModified(); + + /** + * Sets the model as unmodified. This entails that {@link #isModified()} will return + * false unless the model doesn't get modified again. After this call a new modification + * of the model would trigger the associated change listeners again. + */ + void setUnmodified(); + + /** + * Sorts the nodes and edges is the model. The ordering method is given by a diagram + * element comparator. + * @see DiagramElementComparator + */ + void sort(); + + /** + * Returns 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 + */ + ReentrantLock getMonitor(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ConnectNodesException.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,33 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + + +/** + * Represents the exception that is raised when a number of nodes are + * connected through an edge which allows for a different number of nodes only. + * + * @see DiagramEdge#connect(java.util.List) + */ +@SuppressWarnings("serial") +public class ConnectNodesException extends Exception { + public ConnectNodesException(String message){ + super(message); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramEdge.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,173 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * This class represent an edge in the diagram model. Note that this class is + * a tree node. + * + */ +@SuppressWarnings("serial") +public abstract class DiagramEdge extends DiagramElement { + /** + * + * @param type the type of the edge + * @param availableEndDescriptions the end descriptions this edge allows + */ + public DiagramEdge(String type, String[] availableEndDescriptions){ + setType(type); + this.availableEndDescriptions = availableEndDescriptions; + endLabels = new LinkedHashMap<DiagramNode,String>(); + endDescriptions = new LinkedHashMap<DiagramNode,Integer>(); + } + + /** + * Returns a more detailed description of the edge than {@link #spokenText()}. + * the description includes which nodes this edge is connecting + * + * @return a description of the edge + */ + @Override + public String detailedSpokenText(){ + final String and = " and "; + StringBuilder b = new StringBuilder(getType()); + b.append(' ').append(spokenText()); + b.append(". Connecting "); + for(int i=0; i<getChildCount();i++){ + b.append(((DiagramTreeNode)getChildAt(i)).getName()); + b.append(and); + } + // remove the last " and " + b.delete(b.length()-and.length(), b.length()); + return b.toString(); + } + + /** + * Set a label related to a node. On a graphical representation of the diagram + * the label would be put in proximity of the node. + * + * @param n the node,at whose end the label is located + * @param label the label + * @param source the source of the action that triggered this method + */ + public void setEndLabel(DiagramNode n, String label, Object source){ + if(label == null) + label = ""; + endLabels.put(n, label); + notifyChange(new ElementChangedEvent(this,n,"endLabel",source)); + } + + /** + * Returns the end label related to a node. On a graphical representation of the diagram + * the label would be put in proximity of the node. + * + * @param n the node, at whose end the label is located + * + * @return the label at the specified end + */ + public String getEndLabel(DiagramNode n){ + String s = endLabels.get(n); + if(s == null) + return ""; + return s; + } + + /** + * Returns an array with all the available end description this edge can be + * assigned. + * @return an array of string available end description + */ + public String[] getAvailableEndDescriptions(){ + return this.availableEndDescriptions; + } + + /** + * Set a string describing the end related to a node. On a visual diagram this + * corresponds to an arrow. + * + * @param n the node at the edge end whose description will be set + * @param index an index of the array returned by getAvailableEndDescriptions(). The + * edge end description will be set with the string at that position in the array. + * if index is equal to NO_END_DESCRIPTION_INDEX, then the description will be set + * as the empty string. + * @param source the source of the action that triggered this method + * + */ + public void setEndDescription(DiagramNode n, int index, Object source){ + endDescriptions.put(n, index); + notifyChange(new ElementChangedEvent(this,n,"arrowHead",source)); + } + + /** + * Returns a string describing the end related to a node. On a visual diagram this + * corresponds to an arrow. + * + * @param n the node at the edge end whose description will be returned + * @return a description string + */ + public String getEndDescription(DiagramNode n){ + Integer index = endDescriptions.get(n); + if(index == null || index == NO_END_DESCRIPTION_INDEX) + return ""; + return availableEndDescriptions[endDescriptions.get(n)]; + } + + /** + * Returns the connected node at the specified index + * @param index an index into node's list + * @return the connected node at the specified index + */ + public abstract DiagramNode getNodeAt(int index); + + /** + * Returns the number of nodes this edge is connecting + * @return the number of nodes this edge is connecting + */ + public abstract int getNodesNum(); + + /** + * Removes a node from this edge. On a graphical representation of the diagram + * this would mean that the node is no longer connected to the other nodes via this edge. + * @param n the node to be removed + * @param source the source of this action + * @return true if the inner collection changed as a result of the call + */ + public abstract boolean removeNode(DiagramNode n, Object source); + + /** + * Connect a list of nodes with this edge + * @param nodes a list of nodes to connect + * @throws ConnectNodesException if the number of nodes in the list is different + * from the number allowed by this edge. + */ + public abstract void connect(List<DiagramNode> nodes) throws ConnectNodesException; + + /** + * An index to be passed to {@link #setEndDescription(DiagramNode, int, 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; + private Map<DiagramNode,String> endLabels; + private Map<DiagramNode,Integer> endDescriptions; + private String[] availableEndDescriptions; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramElement.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,184 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.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 + * class which is extended by DiagramEdge and DiagramNode. + * + */ +@SuppressWarnings("serial") +public abstract class DiagramElement extends DiagramTreeNode implements Cloneable{ + + protected DiagramElement(){ + name = ""; + id = NO_ID; + notifier = DUMMY_NOTIFIER; // initially set to no effect notifier + } + + /** + * Returns the type of this diagram element. The type is 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(){ + return type; + } + + /** + * Set the type of this diagram element. This method should be called as soon as the object is created + * and should not be called anymore on this object. + * + * @param type the type of this element + */ + protected void setType(String type){ + this.type = type; + } + + /** + * Notifies the model of a changed that has happened on this element. If this element is not + * held by any model than this method will have no effect. + * @param evt an event representing the fact that the element is changed + */ + protected void notifyChange(ElementChangedEvent evt){ + notifier.notifyChange(evt); + } + + /** + * returns the tree node name "as it is", without any decoration such as notes, bookmarks or cardinality. + * Unlike the String returned by toString + * @return the tree node name + */ + public String getName(){ + if(name.isEmpty() && id != NO_ID) + return "new " + getType() + " " + id; + return name; + } + + /** + * Sets the name of this element instance. + * + * @param s the string to set as the name + * @param source the source of this action + */ + 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){ + name = "new " + getType() + " " + id; + } + setUserObject(name); + this.name = name; + notifyChange(new ElementChangedEvent(this,this,"name",source)); + } + + /** + * Returns an InputStream to a sound file with the sound of this element + * @return an InputStream + */ + public abstract InputStream getSound(); + + /** + * Sets the if for this element. The id is a number which uniquely identifies this instance + * within a DiagramModel. + * Unlike the name, which can be the same for two different instances. + * @param id a long number which must be greater than 0 + * @throws IllegalArgumentException id the id passe as argument is lower or equal to 0. + */ + public void setId(long id){ + if (id < NO_ID) + throw new IllegalArgumentException(); + else + this.id = id; + if(name.isEmpty() && id != NO_ID){ + String s = "new " + getType() + " " + id; + this.name = s; + setUserObject(s); + } + } + + /** + * Returns the id of this instance of DiagramElement. + * @return a long representing the id of this instance of the element + * or NO_ID if it hasn't got one. + */ + public long getId(){ + return id; + } + + 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 + */ + <N extends DiagramNode,E extends DiagramEdge> void setNotifier(DiagramModel<N,E>.ReentrantLockNotifier notifier){ + this.notifier = notifier; + } + + @Override + public Object clone(){ + DiagramElement clone = (DiagramElement)super.clone(); + clone.name = ""; + clone.id = NO_ID; + return clone; + } + + /** + * Returns a description of the DiagramElement passed as argument, suitable + * for logging purposes. + * + * @param de the diagram element to log stuff about + * @return a log entry describing the element passed as agument + */ + public static String toLogString(DiagramElement de){ + StringBuilder builder = new StringBuilder(de.getName()); + builder.append('('); + if(de.getId() == DiagramElement.NO_ID) + builder.append("no id"); + else + builder.append(de.getId()); + builder.append(')'); + return builder.toString(); + } + + private long id = NO_ID; + private ElementNotifier notifier; + private String type; + private String name; + private static final ElementNotifier DUMMY_NOTIFIER = new ElementNotifier(){ + @Override + public void notifyChange(ElementChangedEvent evt) {} + }; + /** + * The value returned by getId() if the element instance has not been assigned any id + */ + public static final long NO_ID = 0; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramElementComparator.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,46 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.Comparator; + +/** + * A Comparator for diagram elements. The elements are ordered by their id. + * + * @see DiagramElement#getId() + */ +public class DiagramElementComparator implements Comparator<DiagramElement> { + public static DiagramElementComparator getInstance(){ + if(comparator == null) + comparator = new DiagramElementComparator(); + return comparator; + } + + @Override + public int compare(DiagramElement de1, DiagramElement de2) { + if(de1.getId() == de2.getId()) + return 0; + else if(de1.getId() < de2.getId()) + return -1; + else + return 1; + } + + private static DiagramElementComparator comparator; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,820 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeNode; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * This class represent a model as per in the model-view control architecture. + * The model is "double sided" in the sense that it can be accessed through either + * a CollectionModel or a TreeModel returned by the respective getter methods. + * The TreeModel is suitable for JTree classes of the swing library, while + * the CollectionModel can be used by view classes by registering a CollectionListener + * to the CollectionModel itself. + * It is important to notice that changes made on one side will reflect on the other, + * eventually triggering the registered listeners. + * The tree model is structured according to a special layout which is suitable for + * browsing the tree view via audio interface ( text to speech synthesis and sound). + * + * @param <N> a subclass of DiagramNode + * @param <E> a subclass of DiagramEdge + */ +public class DiagramModel<N extends DiagramNode, E extends DiagramEdge>{ + /** + * Create a model instance starting from some nodes and edges prototypes. + * All subsequently added element must be clones of such prototypes. + * @param nodePrototypes an array of {@code DiagramNode} prototypes, from which + * nodes that will be inserted in this model will be cloned + * @param edgePrototypes an array of {@code DiagramEdge} prototypes, from which + * edges that will be inserted in this model will be cloned + */ + @SuppressWarnings("serial") + public DiagramModel(N [] nodePrototypes, E [] edgePrototypes) { + root = new DiagramTreeNode(ROOT_LABEL){ + @Override + public boolean isRoot(){ + return true; + } + }; + modified = false; + + nodeCounter = 0; + edgeCounter = 0; + + notifier = new ReentrantLockNotifier(); + + treeModel = new InnerTreeModel(root); + treeModel.setEventSource(treeModel);/* default event source is the tree itself */ + diagramCollection = new InnerDiagramCollection(); + + nodes = new ArrayList<N>(INITIAL_NODES_SIZE); + edges = new ArrayList<E>(INITIAL_EDGES_SIZE); + elements = new ArrayList<DiagramElement>(INITIAL_NODES_SIZE+INITIAL_EDGES_SIZE); + + changeListeners = new LinkedList<ChangeListener>(); + + for(N n : nodePrototypes) + addType(n); + for(E e : edgePrototypes){ + addType(e); + } + } + + /** + * Returns a CollectionModel for this diagram + * + * @return a CollectionModel for this diagram + */ + public CollectionModel<N,E> getDiagramCollection(){ + return diagramCollection; + } + + /** + * Returns a TreeModel for this diagram + * + * @return a TreeModel for this diagram + */ + public TreeModel<N,E> getTreeModel(){ + return treeModel; + } + + private void 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){ + DiagramTreeNode typeNode = _lookForChild(root, element.getType()); + if(typeNode == null){ + typeNode = new TypeMutableTreeNode(element); + treeModel.insertNodeInto(typeNode, root, root.getChildCount()); + } + } + + private class InnerDiagramCollection implements CollectionModel<N,E> { + + public InnerDiagramCollection(){ + listeners = new ArrayList<CollectionListener>(); + } + + @Override + public boolean insert(N n, Object source){ + if(source == null) + source = this; + return _insert(n,source); + } + + @Override + public boolean insert(E e, Object source){ + if(source == null) + source = this; + return _insert(e,source); + } + + @Override + public boolean takeOut(DiagramElement element, Object source){ + if(source == null) + source = this; + if(element instanceof DiagramNode) + return _takeOut((DiagramNode)element,source); + if(element instanceof DiagramEdge) + return _takeOut((DiagramEdge)element,source); + return false; + } + + @Override + public void addCollectionListener(CollectionListener listener) { + listeners.add(listener); + } + + @Override + public void removeCollectionListener(CollectionListener listener) { + listeners.remove(listener); + } + + protected void fireElementInserted(Object source, DiagramElement element) { + for(CollectionListener l : listeners){ + l.elementInserted(new CollectionEvent(source,element)); + } + } + + protected void fireElementTakenOut(Object source, DiagramElement element) { + for(CollectionListener l : listeners){ + l.elementTakenOut(new CollectionEvent(source,element)); + } + } + + protected void fireElementChanged(ElementChangedEvent evt){ + for(CollectionListener l : listeners){ + l.elementChanged(evt); + } + } + + @Override + public Collection<N> getNodes() { + return Collections.unmodifiableCollection(nodes); + } + + @Override + public Collection<E> getEdges() { + return Collections.unmodifiableCollection(edges); + } + + @Override + public Collection<DiagramElement> getElements(){ + return Collections.unmodifiableCollection(elements); + } + + @Override + public 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; + } + + protected ArrayList<CollectionListener> listeners; + + } + + @SuppressWarnings("serial") + private class InnerTreeModel extends DefaultTreeModel implements TreeModel<N,E>{ + + public InnerTreeModel(TreeNode root){ + super(root); + bookmarks = new LinkedHashMap<String, DiagramTreeNode>(); + diagramTreeNodeListeners = new ArrayList<DiagramTreeNodeListener>(); + } + + @Override + 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,source); + } + else{ + result = _takeOut((DiagramNode)treeNode,source); + } + /* remove the bookmarks associated with the just deleted diagram element, if any */ + for(String key : treeNode.getBookmarkKeys()) + bookmarks.remove(key); + return result; + } + + @Override + public DiagramTreeNode putBookmark(String bookmark, DiagramTreeNode treeNode, Object source){ + if(bookmark == null) + throw new IllegalArgumentException("bookmark cannot be null"); + if(source == null) + source = this; + setEventSource(source); + treeNode.addBookmarkKey(bookmark); + DiagramTreeNode result = bookmarks.put(bookmark, treeNode); + nodeChanged(treeNode); + iLog("bookmark added",bookmark); + DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,bookmark,source); + for(DiagramTreeNodeListener l : diagramTreeNodeListeners){ + l.bookmarkAdded(evt); + } + handleChangeListeners(this); + return result; + } + + @Override + public DiagramTreeNode getBookmarkedTreeNode(String bookmark) { + return bookmarks.get(bookmark); + } + + @Override + 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); + DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,bookmark,source); + for(DiagramTreeNodeListener l : diagramTreeNodeListeners){ + l.bookmarkRemoved(evt); + } + handleChangeListeners(this); + return treeNode; + } + + @Override + public Set<String> getBookmarks(){ + return new LinkedHashSet<String>(bookmarks.keySet()); + } + + @Override + public void setNotes(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")); + 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 ReentrantLock getMonitor(){ + return notifier; + } + + @Override + public void addDiagramTreeNodeListener(DiagramTreeNodeListener l){ + diagramTreeNodeListeners.add(l); + } + + @Override + 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 + protected void fireTreeNodesChanged(Object source, Object[] path, + int[] childIndices, Object[] children) { + super.fireTreeNodesChanged(src, path, childIndices, children); + } + + @Override + protected void fireTreeNodesInserted(Object source, Object[] path, + int[] childIndices, Object[] children) { + super.fireTreeNodesInserted(src, path, childIndices, children); + } + + @Override + protected void fireTreeNodesRemoved(Object source, Object[] path, + int[] childIndices, Object[] children) { + super.fireTreeNodesRemoved(src, path, childIndices, children); + } + + @Override + protected void fireTreeStructureChanged(Object source, Object[] path, + int[] childIndices, Object[] children) { + super.fireTreeStructureChanged(src, path, childIndices, children); + } + + public boolean isModified(){ + return modified; + } + + public void setUnmodified(){ + modified = false; + } + + private Object src; + private Map<String, DiagramTreeNode> bookmarks; + private ArrayList<DiagramTreeNodeListener> diagramTreeNodeListeners; + } + + @SuppressWarnings("serial") + class ReentrantLockNotifier extends ReentrantLock implements ElementNotifier { + @Override + public void notifyChange(ElementChangedEvent evt) { + _change(evt); + handleChangeListeners(evt.getDiagramElement()); + } + } + + 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){ + 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); + + /* insert node into tree which fires tree listeners */ + treeModel.insertNodeInto(n, parent, parent.getChildCount()); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(parent); + diagramCollection.fireElementInserted(source,n); + handleChangeListeners(n); + + iLog("node inserted",DiagramElement.toLogString(n)); + return true; + } + + 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); + /* remove the node from the tree (fires listeners) */ + treeModel.removeNodeFromParent(n); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(n.getParent()); + /* remove the nodes from the collection */ + nodes.remove(n); + elements.remove(n); + /* notify all the listeners a new node has been removed */ + diagramCollection.fireElementTakenOut(source,n); + handleChangeListeners(n); + + if(nodes.isEmpty()){ + nodeCounter = 0; + }else{ + long lastNodeId = nodes.get(nodes.size()-1).getId(); + if(n.getId() > lastNodeId) + nodeCounter = lastNodeId; + } + iLog("node removed",DiagramElement.toLogString(n)); + return true; + } + + private 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 */ + 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*/ + treeModel.insertNodeInto(new EdgeReferenceMutableTreeNode(e,n), edgeType, 0); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(edgeType); + + n.addEdge(e); + /* insert the node reference into the edge tree node */ + e.insert(new NodeReferenceMutableTreeNode(n,e), 0); + } + + 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); + handleChangeListeners(e); + + StringBuilder builder = new StringBuilder(DiagramElement.toLogString(e)); + builder.append(" connecting:"); + for(int i=0; i<e.getNodesNum();i++) + builder.append(DiagramElement.toLogString(e.getNodeAt(i))).append(' '); + + iLog("edge inserted",builder.toString()); + return true; + } + + private boolean _takeOut(DiagramEdge e, Object source) { + treeModel.setEventSource(source); + /* update the nodes attached to this edge */ + _clearEdgeReferences(e); + /* remove the edge from the collection */ + edges.remove(e); + elements.remove(e); + /* remove the edge from the tree (fires tree listeners) */ + treeModel.removeNodeFromParent(e); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(e.getParent()); + /* notify listeners for collection */ + diagramCollection.fireElementTakenOut(source,e); + handleChangeListeners(e); + + if(edges.isEmpty()){ + edgeCounter = 0; + }else{ + long lastEdgeId = edges.get(edges.size()-1).getId(); + if(e.getId() > lastEdgeId) + edgeCounter = lastEdgeId; + } + iLog("edge removed",DiagramElement.toLogString(e)); + return true; + } + + private void _removeInternalNodes(DiagramNode n, Object source){ + for(int i=0; i<n.getInternalNodesNum(); i++){ + DiagramNode innerNode = n.getInternalNodeAt(i); + _clearNodeReferences(innerNode, source); + _removeInternalNodes(innerNode, source); + n.removeInternalNode(innerNode); + nodes.remove(n); + } + } + + /* removes both inner and tree node references to an edge from nodes it's attached to */ + private void _clearEdgeReferences(DiagramEdge e){ + for(int i=0; i<e.getNodesNum();i++){ + DiagramNode n = e.getNodeAt(i); + EdgeReferenceMutableTreeNode reference; + /* find the category tree node under which our reference is */ + + reference = _lookForEdgeReference(n, e); + assert(reference != null); + + treeModel.removeNodeFromParent(reference); + DiagramTreeNode type = _lookForChild(n, e.getType()); + if(type.isLeaf()) + treeModel.removeNodeFromParent(type); + n.removeEdge(e); + } + } + + /* removes references from node */ + private void _clearNodeReferences(DiagramNode n, Object source){ + /* remove the node itself from its external node, if any */ + if(n.getExternalNode() != null) + n.getExternalNode().removeInternalNode(n); + /* remove edges attached to this node from the collection */ + ArrayList<DiagramEdge> edgesToRemove = new ArrayList<DiagramEdge>(edges.size()); + for(int i=0; i<n.getEdgesNum(); i++){ + DiagramEdge e = n.getEdgeAt(i); + if(e.getNodesNum() == 2){ // deleting a node on a two ends edge means deleting the edge itself + edgesToRemove.add(e); + }else{ + e.removeNode(n,source); + DiagramTreeNode nodeTreeReference = _lookForNodeReference(e, n); + treeModel.removeNodeFromParent(nodeTreeReference); + } + + } + /* remove the edges that must no longer exist as were two ended edges attached to this node */ + for(DiagramEdge e : edgesToRemove) + _takeOut(e, source); + } + + /* the tree structure is changed as a consequence of this method, therefore it's synchronized * + * so that external classes accessing the tree can get exclusive access through getMonitor() */ + private void _change(ElementChangedEvent evt){ + synchronized(this){ + String changeType = evt.getChangeType(); + /* don't use the event source as it might collide with other threads as + * changes on the collections and inner changes on the node are synch'ed thought different monitors */ + /* treeModel.setEventSource(evt.getSource());*/ + if("name".equals(changeType)){ + if(evt.getDiagramElement() instanceof DiagramNode){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + for(int i=0; i<n.getEdgesNum(); i++){ + DiagramEdge e = n.getEdgeAt(i); + treeModel.nodeChanged(_lookForNodeReference(e,n)); + treeModel.nodeChanged(_lookForEdgeReference(n,e)); + for(int j=0; j<e.getNodesNum(); j++){ + DiagramNode n2 = e.getNodeAt(j); + if(n2 != n) + treeModel.nodeChanged(_lookForEdgeReference(n2,e)); + } + } + iLog("node name changed",DiagramElement.toLogString(n)); + }else{ + DiagramEdge e = (DiagramEdge)evt.getDiagramElement(); + for(int i=0; i<e.getNodesNum();i++){ + DiagramNode n = e.getNodeAt(i); + treeModel.nodeChanged(_lookForEdgeReference(n,e)); + } + iLog("edge name changed",DiagramElement.toLogString(e)); + } + treeModel.nodeChanged(evt.getDiagramElement()); + }else if("properties".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + for(String type : n.getProperties().getTypes()){ + PropertyTypeMutableTreeNode typeNode = null; + for(int i=0; i<n.getChildCount();i++){ + /* find the child treeNode corresponding to the current type */ + if(n.getChildAt(i) instanceof PropertyTypeMutableTreeNode) + if(type.equals(((PropertyTypeMutableTreeNode)n.getChildAt(i)).getType())){ + typeNode = (PropertyTypeMutableTreeNode)n.getChildAt(i); + break; + } + } + + if(typeNode == null) + throw new IllegalArgumentException("Inserted Node property type "+ type + " not present in the tree" ); + + /* set the name and modifier string of all the children PropertyNodes */ + typeNode.setValues(n.getProperties().getValues(type), n.getProperties().getModifiers(type)); + } + treeModel.nodeStructureChanged(evt.getDiagramElement()); + iLog("node properties changed",n.getProperties().toString()); + }else if("properties.clear".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + List<String> empty = Collections.emptyList(); + for(int i=0; i<n.getChildCount();i++){ + /* find the child treeNode corresponding to the current type */ + if(n.getChildAt(i) instanceof PropertyTypeMutableTreeNode){ + ((PropertyTypeMutableTreeNode)n.getChildAt(i)).setValues(empty, null); + } + } + treeModel.nodeStructureChanged(evt.getDiagramElement()); + }else if("property.add".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType()); + PropertyMutableTreeNode propertyNode = new PropertyMutableTreeNode(n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex())); + typeNode.add(propertyNode); + treeModel.insertNodeInto(propertyNode, typeNode, args.getPropertyIndex()); + /* this is necessary to increment the child counter displayed between brackets */ + treeModel.nodeChanged(typeNode); + iLog("property inserted",propertyNode.getName()); + }else if("property.set".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType()); + ((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex())) + .setUserObject(n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex())); + treeModel.nodeChanged((typeNode.getChildAt(args.getPropertyIndex()))); + iLog("property changed",n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex())); + }else if("property.remove".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType()); + iLog("property removed",((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex())).getName()); //must do it before actual removing + treeModel.removeNodeFromParent((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex())); + /* remove the bookmark keys associated with this property tree node, if any */ + for(String key : treeModel.getBookmarks()){ + treeModel.bookmarks.remove(key); + } + }else if("property.modifiers".equals(changeType)){ + DiagramNode n = (DiagramNode)evt.getDiagramElement(); + ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments(); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType()); + PropertyMutableTreeNode propertyNode = ((PropertyMutableTreeNode)typeNode.getChildAt(args.getPropertyIndex())); + StringBuilder builder = new StringBuilder(); + Modifiers modifiers = n.getProperties().getModifiers(args.getPropertyType()); + for(int index : modifiers.getIndexes(args.getPropertyIndex())) + builder.append(modifiers.getTypes().get(index)).append(' '); + propertyNode.setModifierString(builder.toString()); + treeModel.nodeChanged(propertyNode); + }else if("arrowHead".equals(changeType)||"endLabel".equals(changeType)){ + /* source is considered to be the node whose end of the edge was changed */ + DiagramNode source = (DiagramNode)evt.getArguments(); + DiagramEdge e = (DiagramEdge)evt.getDiagramElement(); + treeModel.nodeChanged(e); + for(int i=0; i<e.getChildCount();i++){ + NodeReferenceMutableTreeNode ref = (NodeReferenceMutableTreeNode)e.getChildAt(i); + if(ref.getNode() == source){ + treeModel.nodeChanged(ref); + iLog(("arrowHead".equals(changeType) ? "arrow head changed" :"end label changed"), + "edge:"+DiagramElement.toLogString(e)+" node:"+DiagramElement.toLogString(ref.getNode())+ + " value:"+ ("arrowHead".equals(changeType) ? e.getEndDescription(ref.getNode()): e.getEndLabel(ref.getNode()))); + break; + } + } + }else if("notes".equals(changeType)){ + /* do nothing as the tree update is taken care in the tree itself + * and cannot do it here because it must work for all the diagram tree nodes */ + + } + } + /* do nothing for other ElementChangedEvents as they only concern the diagram listeners */ + /* just forward the event to other listeners which might have been registered */ + diagramCollection.fireElementChanged(evt); + } + + private static DiagramTreeNode _lookForChild(DiagramTreeNode parentNode, String name){ + DiagramTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramTreeNode> children = parentNode.children(); children.hasMoreElements();){ + temp = children.nextElement(); + if(temp.getName().equals(name)){ + child = temp; + break; + } + } + return child; + } + + private static NodeReferenceMutableTreeNode _lookForNodeReference(DiagramEdge parent, DiagramNode n){ + NodeReferenceMutableTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramTreeNode> children = parent.children(); children.hasMoreElements();){ + temp = (NodeReferenceMutableTreeNode)children.nextElement(); + if( ((NodeReferenceMutableTreeNode)temp).getNode().equals(n)){ + child = temp; + break; + } + } + return child; + } + + private static EdgeReferenceMutableTreeNode _lookForEdgeReference( DiagramNode parentNode, DiagramEdge e){ + DiagramTreeNode edgeType = _lookForChild(parentNode, e.getType()); + assert(edgeType != null); + EdgeReferenceMutableTreeNode child = null, temp; + for(@SuppressWarnings("unchecked") + Enumeration<DiagramTreeNode> children = edgeType.children(); children.hasMoreElements();){ + temp = (EdgeReferenceMutableTreeNode)children.nextElement(); + if( ((EdgeReferenceMutableTreeNode)temp).getEdge().equals(e)){ + child = temp; + break; + } + } + return child; + } + + private void iLog(String action,String args){ + InteractionLog.log("MODEL",action,args); + } + + private DiagramTreeNode root; + private InnerDiagramCollection diagramCollection; + private ArrayList<N> nodes; + private ArrayList<E> edges; + private ArrayList<DiagramElement> elements; + private InnerTreeModel treeModel; + + private long edgeCounter; + private long nodeCounter; + + private ReentrantLockNotifier notifier; + private List<ChangeListener> changeListeners; + + private boolean modified; + + private final static String ROOT_LABEL = "Diagram"; + private final static int INITIAL_EDGES_SIZE = 20; + private final static int INITIAL_NODES_SIZE = 30;}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,263 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.List; +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent.PropertyChangeArgs; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; + +/** + * This class represents a node in the diagram. + * + */ +@SuppressWarnings("serial") +public abstract class DiagramNode extends DiagramElement { + /** + * Constructor to be called by sub classes + * + * @param type the type of the new node. All nodes with this type will be + * put under the same tree node in the tree representation + * @param properties the properties of this node + */ + public DiagramNode(String type, NodeProperties properties){ + setType(type); + this.properties = properties; + } + + /** + * Returns the properties of this node. Be aware that what is returned is the reference + * to the actual NodeProperties object inside this DiagramNode. Thereforemodifying the returned + * object will affect this node. + * @return the properties of this node + */ + public NodeProperties getProperties(){ + return properties; + } + + /** + * Returns a copy of the properties of this node. The modifying the returned object + * won't affect this Node. + * @return Returns a copy of the properties of this node + */ + public NodeProperties getPropertiesCopy(){ + NodeProperties p = (NodeProperties)properties.clone(); + for(String type : properties.getTypes()){ + Modifiers modifiers = properties.getModifiers(type); + int index = 0; + for(String value : properties.getValues(type)){ + if(properties.getModifiers(type).isNull()) + p.addValue(type, value); + else + p.addValue(type, value, modifiers.getIndexes(index++)); + } + } + return p; + } + + /** + * Set the NodeProperties of this node + * @param properties the properties to set for this node + * @param source the source of the action that triggered this method + */ + public void setProperties(NodeProperties properties, Object source){ + this.properties = properties; + notifyChange(new ElementChangedEvent(this,this.properties,"properties",source)); + } + + /** + * Add a property to the NodeProperties of this node + * @see NodeProperties#addValue(String, String) + * + * @param propertyType the type of the property to add + * @param propertyValue the property to add + * @param source the source of the action that triggered this method + */ + public void addProperty(String propertyType, String propertyValue, Object source){ + getProperties().addValue(propertyType, propertyValue); + int index = getProperties().getValues(propertyType).size() - 1; + 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) + * + * @param propertyType the type of the property to add + * @param valueIndex the index of the property to remove + * @param source the source of the action that triggered this method + */ + public void removeProperty(String propertyType, int valueIndex, Object source){ + String oldValue = getProperties().getValues(propertyType).get(valueIndex); + getProperties().removeValue(propertyType, valueIndex); + 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) + * + * @param propertyType the type of the property to add + * @param valueIndex the index of the property to remove + * @param newValue the new value for this property + * @param source the source of the action that triggered this method + */ + 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(this,new PropertyChangeArgs(propertyType,valueIndex,oldValue),"property.set",source)); + } + + /** + * Removes all the values in the NodeProperties of this node + * @see NodeProperties#clear() + * + * @param source the source of the action that triggered this method + */ + public void clearProperties(Object source){ + getProperties().clear(); + notifyChange(new ElementChangedEvent(this,this,"properties.clear",source)); + } + + /** + * set the modifier indexes in the NodeProperties of this node + * @see Modifiers#setIndexes(int, Set) + * + * @param propertyType the type of the property to add + * @param propertyValueIndex the index of the property value whose modifiers + * are to be changed + * @param modifierIndexes the new modifiers (identified by their index ) + * for this property value + * @param source the source of the action that triggered this method + */ + public void setModifierIndexes(String propertyType, int propertyValueIndex, Set<Integer> modifierIndexes,Object source){ + StringBuilder oldIndexes = new StringBuilder(); + List<String> modifierTypes = getProperties().getModifiers(propertyType).getTypes(); + Set<Integer> indexes = getProperties().getModifiers(propertyType).getIndexes(propertyValueIndex); + for(Integer I : indexes){ + oldIndexes.append(modifierTypes.get(I)).append(' '); + } + getProperties().getModifiers(propertyType).setIndexes(propertyValueIndex, modifierIndexes); + notifyChange(new ElementChangedEvent(this,new PropertyChangeArgs(propertyType,propertyValueIndex,oldIndexes.toString()),"property.modifiers",source)); + } + + /** + * Returns a more detailed description of the node than {@link #spokenText()}. + * the description includes which how many properties the node has and + * how many edges are attached to it. + * + * @return a description of the node + */ + @Override + public String detailedSpokenText(){ + StringBuilder builder = new StringBuilder(getType()); + builder.append(' '); + builder.append(getName()); + builder.append('.').append(' '); + for(int i=0; i<getChildCount();i++){ + DiagramTreeNode treeNode = (DiagramTreeNode) getChildAt(i); + if(treeNode.getChildCount() > 0){ + builder.append(treeNode.getChildCount()) + .append(' ') + .append(treeNode.getName()) + .append(';') + .append(' '); + } + } + return builder.toString(); + } + + /** + * Returns the number of attached edges + * @return the number of attached edges + */ + public abstract int getEdgesNum(); + + /** + * Returns the attached edge at the specified index + * @param index an index into edge's list + * @return the attached edge at the specified index + */ + public abstract DiagramEdge getEdgeAt(int index); + + /** + * add an edge to the attached edges list + * @param e the edge to be added + * + * @return {@code true} if internal collection of edges changed as a result of the call + */ + public abstract boolean addEdge(DiagramEdge e); + + /** + * Removes an edge from the attached edges list + * @param e the edge to be removed + * + * @return {@code true} if internal collection of edges changed as a result of the call + */ + public abstract boolean removeEdge(DiagramEdge e); + + /** + * Gets the node this node is within. + * @return the parent node, or null if the node + * has no parent + */ + public abstract DiagramNode getExternalNode(); + + /** + * Sets the node this node is within. + * @param node the parent node, or null if the node + * has no parent + */ + public abstract void setExternalNode(DiagramNode node); + + /** + * Returns the number of internal nodes + * @return the number of internal nodes + */ + public abstract int getInternalNodesNum(); + + /** + * Gets the internal node at the specified index + * @param index an index into internal nodes list + * @return the internal node at the specified index + */ + public abstract DiagramNode getInternalNodeAt(int index); + + /** + * Adds a node to the internal nodes list. + * @param node the internal node to add + */ + public abstract void addInternalNode(DiagramNode node); + + /** + * Removes a node from the internal nodes list. + * @param node the internal node to remove + */ + public abstract void removeInternalNode(DiagramNode node); + + @Override + public Object clone(){ + DiagramNode clone = (DiagramNode)super.clone(); + clone.properties = (NodeProperties)properties.clone(); + return clone; + } + + private NodeProperties properties; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,292 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +/** + * This class represent a general node in a TreeModel + * + */ +@SuppressWarnings("serial") +public abstract class DiagramTreeNode extends DefaultMutableTreeNode { + /** + * Creates a tree node with the default user object. The default user object has no label. Therefore + * this node will have no label when displayed on a tree. + */ + public DiagramTreeNode() { + super(); + notes = ""; + userObject = new UserObject(); + setSuperClassUserObject(userObject); + bookmarkKeys = new ArrayList<String>(); + } + + /** + * Creates a tree node, holding the user object passed as argument. The label of the + * tree node will be the string returned by {@code userObject.toString()} + * + * @param userObject the user object for this tree node + * + * @see javax.swing.tree.DefaultMutableTreeNode + */ + public DiagramTreeNode(Object userObject) { + this(); + setUserObject(userObject); + } + + /** + * Each DiagramModelTreeNode keeps track of the bookmarks it has been assigned. Bookmarks + * will affect how this tree node will be represented on a JTree: when a tree node is bookmarked + * an apex appears at the right of its name. + * + * @param key the bookmark + * @return true if this bookmark inner collection changed as a result of the call + */ + boolean addBookmarkKey(String key){ + return bookmarkKeys.add(key); + } + + /** + * Removes a bookmark key from the inner collection. + * + * @param key the key to remove + * @return true if this bookmark inner collection changed as a result of the call + */ + boolean removeBookmarkKey(String key){ + return bookmarkKeys.remove(key); + } + + /** + * Returns the the bookmark keys currently associated to this tree node in the tree model. + * + * @return a n unmodifiable list of strings used as keys for bookmarks + */ + public List<String> getBookmarkKeys(){ + return Collections.unmodifiableList(bookmarkKeys); + } + + public String getNotes(){ + return notes; + } + + /** + * Set a note for this tree node. A Note is a text the user wants to attach to a tree node. Notes + * will affect how this tree node will be represented on a JTree: when a tree node is assigned a note + * a number sign (#) appears at the right of its name. + * + * @param note the text of the note + * @param source used by {@code DiagramElement} to trigger {@code ElementChangeEvents} + * + * @see DiagramElement#setNotes(String, Object) + */ + protected void setNotes(String note, Object source){ + this.notes = note; + } + + @Override + public DiagramTreeNode getParent(){ + return (DiagramTreeNode)super.getParent(); + } + + @Override + public DiagramTreeNode getChildAt(int i){ + return (DiagramTreeNode)super.getChildAt(i); + } + + @Override + public DiagramTreeNode getRoot(){ + return (DiagramTreeNode)super.getRoot(); + } + + @Override + public void setUserObject(Object userObject){ + ((UserObject)this.userObject).setObject(userObject); + } + + @Override + public Object getUserObject(){ + return userObject; + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * + * @return a String suitable for text to speech synthesis + */ + public String spokenText(){ + return ((UserObject)userObject).spokenText(); + } + + /** + * Returns a more detailed description of the tree node than {@link #spokenText()}. + * + * @return a description of the tree node + */ + public String detailedSpokenText(){ + return spokenText(); + } + + /** + * returns the tree node name "as it is", without any decoration such as notes, bookmarks or cardinality; + * unlike the String returned by toString. + * + * @return the tree node name + */ + public String getName(){ + return ((UserObject)userObject).getName(); + } + + @Override + public boolean isRoot(){ + return false; // root node overwrites this method + } + + @Override + public DiagramTreeNode getLastLeaf() { + return (DiagramTreeNode)super.getLastLeaf(); + } + + @Override + public DiagramTreeNode getNextLeaf() { + return (DiagramTreeNode)super.getNextLeaf(); + } + + @Override + public DiagramTreeNode getNextNode() { + return (DiagramTreeNode)super.getNextNode(); + } + + @Override + public DiagramTreeNode getNextSibling() { + return (DiagramTreeNode)super.getNextSibling(); + } + + @Override + public DiagramTreeNode getPreviousLeaf() { + return (DiagramTreeNode)super.getPreviousLeaf(); + } + + @Override + public DiagramTreeNode getPreviousNode() { + return (DiagramTreeNode)super.getPreviousNode(); + } + + @Override + public DiagramTreeNode getPreviousSibling() { + return (DiagramTreeNode)super.getPreviousSibling(); + } + + private void setSuperClassUserObject(Object u){ + super.setUserObject(u); + } + + private UserObject getUserObjectInstance(){ + return new UserObject(); + } + + /** + * The bookmarks, involving this node, entered by the user in the DiagramTree this node belongs to. + */ + protected List<String> bookmarkKeys; + /** + * The notes set by the user for this node. + */ + protected String notes; + /* hides the DefaultMutableTreeNode protected field */ + private Object userObject; + /** + * The character that is appended to the label of this node when the user enters some notes for it. + */ + protected static final char NOTES_CHAR = '#'; + /** + * The character that is appended to the label of this node when it's bookmarked by the user. + */ + protected static final char BOOKMARK_CHAR = '\''; + /** + * The string that is appended to the spoken text of this node when the user enters some notes for it. + * + * @see #spokenText() + */ + protected static final String BOOKMARK_SPEAK = ", bookmarked"; + /** + * The string that is appended to the spoken text of this node when it's bookmarked by the user. + * + * @see #spokenText() + */ + protected static final String NOTES_SPEAK = ", has notes"; + + @Override + public Object clone(){ + DiagramTreeNode clone = (DiagramTreeNode )super.clone(); + clone.notes = ""; + clone.bookmarkKeys = new ArrayList<String>(); + clone.userObject = clone.getUserObjectInstance(); + clone.setSuperClassUserObject(clone.userObject); + return clone; + } + + /* this works as a wrapper for the real user object in order to provide */ + /* decoration on the treeNode label to signal and/or bookmarks */ + private class UserObject { + private Object object; + + public UserObject(){ + object = ""; + } + public void setObject(Object o){ + this.object = o; + } + + @Override + public boolean equals(Object o){ + return this.object.equals(o); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(object.toString()); + if(!"".equals(notes)){ + builder.append(NOTES_CHAR); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_CHAR); + return builder.toString(); + } + + public String spokenText(){ + StringBuilder builder = new StringBuilder(object.toString()); + if(!"".equals(notes)){ + builder.append(NOTES_SPEAK); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_SPEAK); + return builder.toString(); + } + + public String getName(){ + return object.toString(); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramTreeNodeEvent.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,75 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.EventObject; + +/** + * An event that is triggered in the {@code DiagramModel} when an action + * happens that can involve all the tree nodes of the model's {@code DiagramTree} + * rather than only nodes or edges. + * + * Particularly such actions happens when the user set the notes for a tree node or when + * they bookmark a tree node. + * + */ +@SuppressWarnings("serial") +public class DiagramTreeNodeEvent extends EventObject { + /** + * Creates a new event related to a change in a tree node on either notes or bookmarks. + * In order to get the status of the tree node before the change the old value is passed as + * parameter to this constructor. + * For {@code setNotes} this value is the notes before the change. For {@code removeBookmark} it + * will be the bookmark that has just been removed. Finally for {@code putBookmark()} the value is the + * new bookmark. + * + * @see uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel + * + * @param treeNode the tree node where the action happened + * @param value the value that the tree node had before this action. + * @param source the source of the action + */ + public DiagramTreeNodeEvent(DiagramTreeNode treeNode, String value, Object source){ + super(source); + this.treeNode = treeNode; + this.value = value; + } + + /** + * Returns the tree node on which a new note has been set or a bookmark has been added/removed. + * + * @return the tree node this event refers to. + */ + public DiagramTreeNode getTreeNode() { + return treeNode; + } + + /** + * Returns the old value before the action (the old notes for {@code setNotes()}, + * and the bookmark key for {@code putBookmark() and removeBookmark()}. + * + * @return the old value before the action + */ + public String getValue() { + return value; + } + + private DiagramTreeNode treeNode; + private String value; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramTreeNodeListener.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,49 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * The listener interface for receiving {@code DiagramTreeNode} events. This events + * happen when the user changes the state of any tree node in the {@code DiagramTree}, + * not necessarily (but possibly) a {@code DiagramNode} or {@code DiagramEdge}. + * + */ +public interface DiagramTreeNodeListener { + + /** + * Called when a new bookmark is added to {@code DiagramTree}. + * + * @param evt the event object representing a new bookmark insertion in the {@code DiagramTree}. + */ + public void bookmarkAdded(DiagramTreeNodeEvent evt); + + /** + * Called when a bookmark is removed from the {@code DiagramTree}. + * + * @param evt the event object representing a new bookmark insertion in the {@code DiagramTree}. + */ + public void bookmarkRemoved(DiagramTreeNodeEvent evt); + + /** + * Called when a note is set on a node in the {@code DiagramTree}. + * + * @param evt evt the event object representing a note set on a node in the {@code DiagramTree}. + */ + public void notesChanged(DiagramTreeNodeEvent evt); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceHolderMutableTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,57 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * This class is a special tree node which holds the EdgeReferenceMutableTreeNode + * in the tree layout, where It is normally placed as a {@code DiagramNode}'s child. + */ +@SuppressWarnings("serial") +public class EdgeReferenceHolderMutableTreeNode extends DiagramTreeNode { + + EdgeReferenceHolderMutableTreeNode(Object userObj){ + super(userObj); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(super.toString()); + builder.append(" (").append(getChildCount()).append(")"); + return builder.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 + public String spokenText(){ + StringBuilder builder = new StringBuilder(getName()); + builder.append(", "); + builder.append(getChildCount() == 0 ? "empty" : getChildCount()); + if(!"".equals(notes)){ + builder.append(NOTES_SPEAK); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_SPEAK); + return builder.toString(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/EdgeReferenceMutableTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,86 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * The diagramModeltreeNode placed in a node subtree representing an edge connecting + * that node with another node. + */ +@SuppressWarnings("serial") +public class EdgeReferenceMutableTreeNode extends DiagramTreeNode { + EdgeReferenceMutableTreeNode(DiagramEdge edge, DiagramNode node){ + super(); + this.edge = edge; + this.node = node; + } + + @Override + public String toString(){ + final String and = " and "; + StringBuilder b = new StringBuilder(); + b.append("to "); + for(int i=0;i<edge.getNodesNum();i++){ + DiagramNode n = edge.getNodeAt(i); + if(!n.equals(node)) + b.append(n.getName()).append(and); + } + // remove the last " and " + b.delete(b.length()-and.length(), b.length()); + b.append(", via "); + b.append(edge.getName()); + super.setUserObject(b.toString()); + return super.toString(); + } + + @Override + public String getName(){ + return toString(); + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + toString(); + return super.spokenText(); + } + + /** + * Returns the diagram edge that this tree node represents inside the node subtree + * @return a reference to the actual edge + */ + public DiagramEdge getEdge(){ + return edge; + } + + /** + * Returns the node containing this tree node in its subtree. Notice that + * diagram nodes are DiagrammodelTreeNode as well. + * @return a reference to the diagram node + */ + public DiagramNode getNode(){ + return node; + } + + private DiagramEdge edge; + private DiagramNode node; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ElementChangedEvent.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,109 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.EventObject; + +/** + * ElementChangedEvent is used to notify the model listeners that an element ({@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 { + + /** + * Creates a new instance of {@code ElementChangedEvent} + * + * @param element the element that has been changed + * @param args the arguments of this change event, if any + * @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; + } + + /** + * A String representing the change type. Subclasses of DiagramNode and DiagramEdge + * can throw their own events by passing as argument a String that describes the change. + * Such events will have no effect in the model but will be fired to all the registered ChangeEventListener. + * + * @return a String describing the change type + */ + public String getChangeType(){ + return changeType; + } + + /** + * Returns the DiagramElement that has been affected by this change + * @return the DiagramElement that has been affected by this change + */ + public DiagramElement getDiagramElement(){ + return element; + } + + /** + * 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; + 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; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/ElementNotifier.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,30 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * An elementNotifier is used by a DiagramElement to make the model aware + * that it has been changed somehow (e.g. it has a new name). + * A Reference to the model's ElementNotifier is set into the DiagramElement + * as soon as it's inserted in the model + * + */ +public interface ElementNotifier { + void notifyChange(ElementChangedEvent evt); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/MalformedEdgeException.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,56 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * This RuntimeException is thrown when an edge is inserted in the model without being connected to + * any nodes before. + * + */ +@SuppressWarnings("serial") +public class MalformedEdgeException extends RuntimeException { + + /** + * Creates a new {@code MalformedEdgeException} holding the message passed as argument. + * The message can be accessed by calling {@code getMessage()} on this exception. + * + * @param message the message of this Exception + */ + public MalformedEdgeException(String message) { + super("Edge inserted into data structure was malformed for this reason:" + message); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeProperties.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,543 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class represents the internal properties of a node. Internal properties can be seen as + * attributes that each single nodes owns and that, in a visual diagram, would normally be displayed inside + * or in the neighbourhood of the node itself. This is a very high abstraction of the concept of properties + * of a node as the user can define their own type of properties through the property type definition object + * passed as argument in the constructor. + * An example of properties is <i>attributes</i> and <i>methods</i> of a class diagram in the UML language or just the + * <i>attributes</i> of entities in an ER diagram. + * + */ +public final class NodeProperties implements Cloneable { + + /** + * Creates a diagram element property data structure out of a property type specification. In a UML + * diagram the NodeProperties for a class node would be constructed passing as arguments a linked hash + * map containing the strings "attributes" and "properties" and the strings "public", "protected", "private", + * "static" as value for both the keys. "attributes" and "methods" would be the types of the NodeProperties, + * that is, all the values inserted in the object such as values would fall under either type. For example + * if a UML diagram class has the methods getX() and getY(), these would be inserted in the NodeProperties + * object with a type "methods". + * + * @param typeDefinition a linked Hash Map holding the properties types as keys + * and the modifiers type definition of each property as values + */ + + public NodeProperties(LinkedHashMap<String,Set<String>> typeDefinition){ + if(typeDefinition == null) + this.typeDefinition = EMPTY_PROPERTY_TYPE_DEFINITION; + else + this.typeDefinition = typeDefinition; + /* create the type collection out of the typeDefinition keyset */ + types = Collections.unmodifiableList(new ArrayList<String>(this.typeDefinition.keySet())); + properties = new LinkedHashMap<String,Entry>(); + for(String s : types){ + Entry q = new Entry(); + q.values = new ArrayList<String>(); // property values to be filled by user + modifiers + q.indexes = new ArrayList<Set<Integer>>(); + q.view = null; + Set<String> modifierTypeDefinition = this.typeDefinition.get(s); + if(modifierTypeDefinition == null) + /* null modifiers for this property */ + q.modifiers = new Modifiers(EMPTY_MODIFIER_TYPE_DEFINITION,q.indexes); + else if(modifierTypeDefinition.size() == 0) + /* null modifiers for this property */ + q.modifiers = new Modifiers(EMPTY_MODIFIER_TYPE_DEFINITION,q.indexes); + else + q.modifiers = new Modifiers(modifierTypeDefinition, q.indexes); + properties.put(s, q); + } + } + + /** + * Returns the type definition argument of the constructor this object has been created through. + * + * @return the type definition + */ + public Map<String,Set<String>> getTypeDefinition(){ + return typeDefinition; + } + + /** + * Returns the types of properties. + * + * @return a list of strings holding types of properties + */ + public List<String> getTypes(){ + return types; + } + + /** + * @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 + * {@code null} if the specified type does not exist. + */ + public List<String> getValues(String propertyType){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + return new ArrayList<String>(e.values); + } + + /** + * Returns the view object associated with a property type in this NodeProperties instance. A view object is + * defined by the client of this class and it holds the information needed for a visual representation + * of the property. + * + * @param type the property type the returned view is associated with + * @return the View object or null if non has been set previously + */ + public Object getView(String type){ + Entry e = properties.get(type); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type); + return e.view; + } + + /** + * Sets the View object for the a type of properties. The NodeProperties only works + * as a holder as for the view objects. The client code of this class has to define + * its own view. + * + * @param type the type of property the view is associated with. + * @param o an object defined by user + * @see #getView(String) + */ + public void setView(String type, Object o){ + Entry e = properties.get(type); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type); + e.view = o; + } + + /** + * Returns a reference to the modifier object associated with a property type. Changes to the returned + * reference will affect the internal state of the NodeProperty object. + * + * @param propertyType a property type + * @return the modifiers of the specified property type or null if either + * the property type is null or it was not listed in the property type definition + * passed to the constructor + */ + public Modifiers getModifiers(String propertyType){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + return e.modifiers; + } + + /** + * Adds a value of the type passed as argument. + * + * @param propertyType a property type defined in the property type definition passed as argument to the constructor + * @param propertyValue the value to add to the property type + */ + public void addValue(String propertyType, String propertyValue){ + Entry e = properties.get(propertyType); + /* no such property type exists */ + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.values.add(propertyValue); + e.indexes.add(new LinkedHashSet<Integer>()); + } + + /** + * Adds a value of the type passed as argument and sets the modifier indexes for it as for + * {@link Modifiers#setIndexes(int, Set)}. + * + * @param propertyType a property type + * @param propertyValue a property value + * @param modifierIndexes the modifier set of indexes + * + * @throws IllegalArgumentException if propertyType + * is not among the ones in the type definition passed as argument to the constructor + */ + public void addValue(String propertyType, String propertyValue, Set<Integer> modifierIndexes){ + for(Integer i : modifierIndexes) + if((i < 0)||(i >= getModifiers(propertyType).getTypes().size())) + throw new ArrayIndexOutOfBoundsException("Index "+ i + + " corresponds to no Modifier type (modifierType size = "+ + getModifiers(propertyType).getTypes().size()+")" ); + + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.values.add(propertyValue); + e.indexes.add(modifierIndexes); + } + + /** + * Removes the value at the specified index for the specified property type. + * + * @param propertyType a property type + * @param valueIndex the index of the value to remove + * @return the removed value + * @throws IllegalArgumentException if propertyType + * is not among the ones in the type definition passed as argument to the constructor + */ + public String removeValue(String propertyType, int valueIndex){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.indexes.remove(valueIndex); + return e.values.remove(valueIndex); + } + + /** + * Sets the value of a property type at the specified index to a new value. + * + * @param propertyType a property type + * @param valueIndex the index of the value which must be replaced + * @param newValue the new value for the specified index + * @throws IllegalArgumentException if propertyType + * is not among the ones in the type definition passed as argument to the constructor + */ + public void setValue(String propertyType, int valueIndex, String newValue){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.values.set(valueIndex, newValue); + } + + /** + * Removes all the values and modifiers for a specific type. + * + * @param propertyType the type whose property and modifiers must be removed + * @throws IllegalArgumentException if propertyType + * is not among the ones in the type definition passed as argument to the constructor + */ + public void clear(String propertyType){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + e.values.clear(); + e.indexes.clear(); + } + + /** + * Removes all the values and modifiers of this object. + */ + public void clear(){ + for(String type : types){ + clear(type); + } + } + + /** + * Returns true if this NodeProperties contains no values. + * + * @return true if this NodeProperties contains no values + */ + public boolean isEmpty(){ + boolean empty = true; + for(String type : types) + if(!properties.get(type).values.isEmpty()){ + empty = false; + break; + } + return empty; + } + + /** + * Returns true if there are no values for the specified type in this NodeProperties instance. + * + * @param propertyType the property type to be checked for value presence + * @return true if there are no values for the specified type in this NodeProperties instance + */ + public boolean typeIsEmpty(String propertyType){ + Entry e = properties.get(propertyType); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType); + return e.values.isEmpty(); + } + + /** + * true if this NodeProperties object has no types. This can happen if the constructor + * has been called with an empty or null property type definition. + * + * @return true if this NodeProperties object has no types + */ + public boolean isNull(){ + return types.isEmpty(); + } + + /** + * Returns a string representation of types, value and modifiers of this property. Such a + * representation can be passed as argument to {@link #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. + * + * @return a string representation of the values and modifiers of this object + */ + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(String type : types){ + builder.append(TYPE_ENTRY_SEPARATOR).append(type); + int propertyValueIndex = 0; + for(String value : properties.get(type).values){ + builder.append(VALUE_ENTRY_SEPARATOR); + builder.append(value); + for(int modifierIndex : getModifiers(type).getIndexes(propertyValueIndex)){ + builder.append(MODIFIER_ENTRY_SEPARATOR); + builder.append(modifierIndex); + } + propertyValueIndex++; + } + } + return builder.toString(); + } + + /** + * Fills up the this property according to the string passed as arguments + * The string must be generated by calling toString on a NodeProperty instance + * holding the same property types as type, otherwise an IllegalArgumentException + * is likely to be thrown. + * + * @param s the string representation of the property values, such as the one returned + * by toString() + * @throws IllegalArgumentException if s contains property types which + * are not among the ones in the type definition passed as argument to the constructor + */ + public void fill(String s){ + /* clear up previous values */ + clear(); + + /* a property entry is a string with the type of the property followed by value * + * entries of that type. all value entries begin with the VALUE_SEPARATOR. * + * a value entry is a property value followed by its modifiers entries, a modifier entry * + * is a modifier beginning with the MODIFIER_SEPARATOR */ + String propertyEntries[] = s.split(TYPE_ENTRY_SEPARATOR); + for(String propertyEntry : propertyEntries){ + String[] typeEntries = propertyEntry.split(VALUE_ENTRY_SEPARATOR); + String type = typeEntries[0]; + for(int i=1;i<typeEntries.length;i++){ + Entry e = properties.get(type); + if(e == null) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type); + String valueEntries[] = typeEntries[i].split(MODIFIER_ENTRY_SEPARATOR); + String value = valueEntries[0]; + LinkedHashSet<Integer> modifiers = new LinkedHashSet<Integer>(); + for(int j=1;j<valueEntries.length;j++){ + modifiers.add(Integer.valueOf(valueEntries[j])); + } + addValue(type,value,modifiers); + } + } + } + + @Override + @SuppressWarnings(value = "unchecked") + public Object clone(){ + NodeProperties p = null; + try { + p = (NodeProperties)super.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + p.properties = (LinkedHashMap<String,Entry>)properties.clone(); + for(String key : p.properties.keySet()){ + Entry old = p.properties.get(key); + Entry q = new Entry(); + q.values = new ArrayList<String>(); + q.indexes = new ArrayList<Set<Integer>>(); + q.view = old.view; + if(old.modifiers != null){ + q.modifiers = new Modifiers(old.modifiers.modifierTypes, q.indexes); + for(String modifierType : q.modifiers.getTypes()) + q.modifiers.setView(modifierType, old.modifiers.getView(modifierType)); + } + p.properties.put(key, q); + } + return p; + } + + private class Entry{ + List<String> values; + List<Set<Integer>> indexes; + Modifiers modifiers; + Object view; + } + + /** + * A modifier is a label peculiar of a certain subset of properties. For example in + * a UML class diagram one or more methods can be labelled as <i>public</i>, <i>private</i> or <i>protected</i>. + * Had a NodeProperties instance been used to represent the methods of a class node, there would be then + * one modifier for each label: one for <i>public</i>, one for <i>private</i>, and one for <i>protected</i>. To each modifier + * a view-object can be associated, which describes how these labels would be rendered visually. + * Following on from the UML example, a view-object would be used with the protected modifier + * to hold the information that the properties which are assigned such a modifier must be prefixed + * with the '#' sign. + */ + public class Modifiers{ + private Modifiers(Set<String> modifierTypes, List<Set<Integer>> indexes){ + views = new LinkedHashMap<String,Object>(); + indexesRef = indexes; + this.modifierTypes = Collections.unmodifiableList(new ArrayList<String>(modifierTypes)); + for(String modifierType : modifierTypes){ + views.put(modifierType,null); + } + } + + /* only used by NodeProperties.clone() method */ + private Modifiers(List<String> modifierTypes, List<Set<Integer>> indexes){ + views = new LinkedHashMap<String,Object>(); + indexesRef = indexes; + this.modifierTypes = modifierTypes; + for(String modifierType : modifierTypes){ + views.put(modifierType,null); + } + } + + /** + * Returns the list of modifier types, as per the type definition passed as argument to the NodeProperties + * constructor. + * + * @return a list of modifier types + * @see NodeProperties#NodeProperties(LinkedHashMap) + */ + public List<String> getTypes(){ + return modifierTypes; + } + + /** + * Returns the view object associated with a modifier type in this Modifier instance. A view object is + * defined by the client of this class and it holds the information needed for a visual representation + * of the modifier. + * + * @param 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){ + return views.get(modifierType); + } + + /** + * Sets the View object for the a type of modifier. The NodeProperties only works + * as a holder as for the view objects. The client code of this class has to define + * its own view, which will then be used by the client itself to visualise the modifier + * + * @param modifierType the type of modifier the view is associated with. + * @param view an object defined by user defining how this property looks like + * @see #getView(String) + * @throws IllegalArgumentException if modifierType + * is not among the ones in the type definition passed as argument to the constructor + */ + public void setView(String modifierType, Object view){ + if(!views.containsKey(modifierType)) + throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+modifierType); + views.put(modifierType,view); + } + + /** + * Returns the modifier indexes for the specified property value. Each property type can be associated + * with one or more modifier type and each property value can be assigned one or more modifier. The + * integer set returned by this method tells the client code to which modifiers the property value at the index + * passed as argument is assigned. The returned indexes refer to the modifier list returned by {@link #getTypes()} + * + * @param propertyValueIndex the index of the property value for which the set of modifier indexes + * is being queried + * @return a set of indexes of the modifier types the property value at the index passed as argument + * is assigned to + */ + public Set<Integer> getIndexes(int propertyValueIndex){ + Set<Integer> set = indexesRef.get(propertyValueIndex); + return (new LinkedHashSet<Integer>(set)); + } + + /** + * Set the modifier indexes for the property value at the index passed as argument + * + * @param propertyValueIndex the index of the property value + * @param modifierIndexes a set of indexes which refer to the list returned by {@link #getTypes()} + * @throws ArrayIndexOutOfBoundsException if one or more of the indexes in modifierIndexes are lower than 0 or greater + * or equal to the size of the modifier type list returned by {@link #getTypes()}. The same exception is also thrown + * if propertyValueIndex is lower than 0 or greater or equal to the property values list returned by {@link NodeProperties#getValues(String)} + * passing as argument the same property type passed to {@link NodeProperties#getModifiers(String)} to obtain + * this Modifiers instance. + * @see #getIndexes(int) + */ + public void setIndexes(int propertyValueIndex, Set<Integer> modifierIndexes){ + for(Integer i : modifierIndexes) + if((i < 0)||(i >= getTypes().size())) + throw new ArrayIndexOutOfBoundsException("Index "+ i + " corresponds to no Modifier Type (modifierType size = "+getTypes().size()+")" ); + + Set<Integer> m = indexesRef.get(propertyValueIndex); + m.clear(); + m.addAll(modifierIndexes); + } + + /** + * Removes all the modifier indexes for a property value at the specified index. + * + * @param propertyValueIndex the index of the property value + */ + public void clear(int propertyValueIndex){ + Set<Integer> m = indexesRef.get(propertyValueIndex); + m.clear(); + } + + /** + * true if this Modifiers object has no types. This can happen if the constructor + * has been called with an empty or null modifier in the type definition passed as argument + * to the constructor of the NodeProperties object holding this Modifiers object. + * + * @return true if this Modifiers object has no types + */ + public boolean isNull(){ + return modifierTypes.isEmpty(); + } + + private LinkedHashMap<String,Object> views; + private final List<String> modifierTypes; + private List<Set<Integer>> indexesRef; + } + + /* for each property (key) I associate a Couple (value) made out of * + * a list of the property values plus a list of possible modifiers */ + private LinkedHashMap<String,Entry> properties; + private final List<String> types; + private Map<String,Set<String>> typeDefinition; + + private static final LinkedHashMap<String,Set<String>> EMPTY_PROPERTY_TYPE_DEFINITION = new LinkedHashMap<String,Set<String>>(); + private static final Set<String> EMPTY_MODIFIER_TYPE_DEFINITION = Collections.emptySet(); + private static final String ILLEGAL_TYPE_MSG = "argument must be in type definition list: "; + /** + * A special static instance of the class corresponding to the null NodeProperties. A null NodeProperties instance will have isNull() returning true, + * which means it has no property types associated. + * @see #isNull() + */ + public static final NodeProperties NULL_PROPERTIES = new NodeProperties(EMPTY_PROPERTY_TYPE_DEFINITION); + + private final static String TYPE_ENTRY_SEPARATOR = "\n:"; + private final static String VALUE_ENTRY_SEPARATOR = "\n;"; + private final static String MODIFIER_ENTRY_SEPARATOR = "\n,"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeReferenceMutableTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,98 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * The {@code DiagramModeltreeNode} placed in an edge subtree representing a node connected + * by the edge itself. + */ +@SuppressWarnings("serial") +public class NodeReferenceMutableTreeNode extends DiagramTreeNode { + NodeReferenceMutableTreeNode(DiagramNode node, DiagramEdge edge){ + super(); + this.node = node; + this.edge = edge; + } + + + @Override + public String toString(){ + StringBuilder b = new StringBuilder(); + if(edge.getEndDescription(node) != null){ + b.append(' '); + b.append(edge.getEndDescription(node)); + b.append(' '); + } + b.append(node.getName()); + b.append(' '); + if(edge.getEndLabel(node) != null){ + b.append(edge.getEndLabel(node)); + } + /* set the user object so that superclass toString can be called, which + * decorates the string with notes and bookmarks + */ + setUserObject(b.toString()); + return super.toString(); + } + + /** + * Returns the tree node name "as it is", without any decoration such as notes, bookmarks or cardinality + * ,unlike the String returned by toString. + * + * @return the tree node name + */ + @Override + public String getName(){ + return node.getName(); + } + + /** + * Returns a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + toString(); + return super.spokenText(); + } + + /** + * Returns the diagram edge that has this node in its subtree. Note that diagram edges + * are DiagramModelTreeNodes as well. + * + * @return a reference to the diagram edge + */ + public DiagramEdge getEdge(){ + return edge; + } + + /** + * Returns the diagram node that this tree node represents inside the edge subtree. + * + * @return a reference to the actual diagram node + */ + public DiagramNode getNode(){ + return node; + } + + private DiagramNode node; + private DiagramEdge edge; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyMutableTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,64 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * This class represent node properties in the diagram tree representation. In the tree layout they are placed + * in the DiagramNode subtree. Note that a DiagramNode is also a DiagramModelTreeNode. + * @see NodeProperties + */ +@SuppressWarnings("serial") +public class PropertyMutableTreeNode extends DiagramTreeNode { + PropertyMutableTreeNode(){ + super(); + } + + PropertyMutableTreeNode(Object userObject) { + super(userObject); + modifiersString = ""; + } + + /** + * Sets a string to show in the tree that the NodeProperties value this tree node refers to + * has been assigned one or more modifiers. + * + * @param s the modifier string + */ + public void setModifierString(String s){ + modifiersString = s; + } + + @Override + public String toString(){ + return modifiersString + super.toString(); + } + + /** + * Returns a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + return modifiersString + super.spokenText(); + } + + private String modifiersString; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/PropertyTypeMutableTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,109 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.List; +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; + +/** + * This {@code DiagramModelTreeNode} holds all the {@code PropertyMutableTreeNode} instances + * of a certain type in the diagram tree. + * + * @see PropertyMutableTreeNode + */ +@SuppressWarnings("serial") +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; + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(super.toString()); + builder.append(" (").append(getChildCount()).append(")"); + return builder.toString(); + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + StringBuilder builder = new StringBuilder(getName()); + builder.append(", "); + builder.append(getChildCount() == 0 ? "empty" : getChildCount()); + if(!"".equals(notes)){ + builder.append(NOTES_SPEAK); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_SPEAK); + return builder.toString(); + } + + /* creates and/or set the child treeNodes of this type with the property values */ + public void setValues(List<String> values, Modifiers modifiers){ + int diff = getChildCount() - values.size(); + if(diff > 0){ + for(int i=0;i<diff;i++) + remove(getChildCount()-1); + }else if(diff < 0){ + for(int i=0; i<-diff;i++) + add(new PropertyMutableTreeNode()); + } + + PropertyMutableTreeNode child; + for(int i=0; i<getChildCount();i++){ + StringBuilder builder = new StringBuilder(); + if(modifiers != null){ + Set<Integer> indexes = modifiers.getIndexes(i); + for(int index : indexes) + builder.append(modifiers.getTypes().get(index)).append(' '); + } + child = (PropertyMutableTreeNode)getChildAt(i); + child.setUserObject(values.get(i)); + child.setModifierString(builder.toString()); + } + } + + DiagramNode node; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TreeModel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,143 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +/** + * + * Represents the tree side of a DiagramModel instance. + * + * @param <N> a type extending DiagramNode + * @param <E> a type extending DiagramEdge + */ +public interface TreeModel<N extends DiagramNode, E extends DiagramEdge> extends javax.swing.tree.TreeModel { + + /** + * insert a DiagramNode into the diagram model + * + * @param treeNode the DiagramNode to be inserted in the collection + * @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, 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, 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, Object source); + + /** + * + * Add a bookmark for the specified tree node in the internal collection + * + * @param bookmark a bookmark + * @param treeNode the tree node to be bookmarked + * @param source the sorce of the action that triggered this method + * @return previous value associated with specified key, or null if there was no mapping for key. + * @throws IllegalArgumentException if bookmark is null + */ + 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 + */ + DiagramTreeNode getBookmarkedTreeNode(String bookmark); + + /** + * Returns the list of all the bookmarks of this tree model + * @return the list of all the bookmarks + */ + Set<String> getBookmarks(); + + /** + * Remove the bookmark from the bookmark internal collection + * + * @param bookmark the bookmark to remove + * @param source the source of the action that triggered this method + * @return previous value associated with specified key, or null if there was no mapping for key. + */ + DiagramTreeNode removeBookmark(String bookmark, Object source); + + /** + * Set the notes for the specified tree node. Passing an empty string as notes + * means actually to remove the notes from the tree node. + * + * @param treeNode the tree node to be noted + * @param notes the notes to be assigned to the tree node + * @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(DiagramTreeNode treeNode, String notes, Object source); + + /** + * 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 {@code DiagramNodeListener} to add to the model + */ + void addDiagramTreeNodeListener(DiagramTreeNodeListener l); + + /** + * Removes a {@code DiagramNodeListener} from this object. + * @param l a {@code DiagramNodeListener} to remove from ththis object. + */ + void removeDiagramTreeNodeListener(DiagramTreeNodeListener l); + + /** + * Returns true if the model has been modified + * @return true if the model has been modified + */ + boolean isModified(); + + /** + * Sets the model as unmodified. This entails that {@link #isModified()} will return + * false unless the model doesn't get modified again. After this call a new modification + * of the model would trigger the associated change listeners again. + */ + public void setUnmodified(); + + /** + * Returns a reentrant lock that can be used to access the nodes and edges in a synchronized fashion. + * @return a lock object + */ + public ReentrantLock getMonitor(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/TypeMutableTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,69 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.diagrammodel; + +/** + * + * This class is a DiagramModelTreeNode representing a type of a diagram element (node or edge) + * in the diagram tree layout the elements will be children of this tree nodes. + * + */ +@SuppressWarnings("serial") +public class TypeMutableTreeNode extends DiagramTreeNode { + TypeMutableTreeNode(DiagramElement element) { + super(element.getType()); + this.prototype = element; + } + + /** + * Returns a prototype diagram element which can be cloned to create other diagram elements + * of this type. + * @return the prototype diagram element + */ + public DiagramElement getPrototype(){ + return (DiagramElement)prototype.clone(); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(super.toString()); + builder.append(" (").append(getChildCount()).append(")"); + return builder.toString(); + } + + /** + * Return a String representing this object for this tree node in a way more suitable + * for a text to speech synthesizer to read, than toString(). + * @return a String suitable for text to speech synthesis + */ + @Override + public String spokenText(){ + StringBuilder builder = new StringBuilder(getName()); + builder.append(", "); + builder.append(getChildCount() == 0 ? "empty" : getChildCount()); + if(!"".equals(notes)){ + builder.append(NOTES_SPEAK); + } + if(!bookmarkKeys.isEmpty()) + builder.append(BOOKMARK_SPEAK); + return builder.toString(); + } + + private DiagramElement prototype; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/AudioFeedback.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,163 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionListener; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNodeListener; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNodeEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent.PropertyChangeArgs; +import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; + +/** + * This class is a listener providing audio (speech + sound) feedback to changes on the + * model (e.g. node added, node removed, node name changed etc.) operated only on the local (so not from + * a tree of another user sharing the same diagram) + * tree it is linked to. If the source of the events is different from the local tree , then no action + * is performed. + */ +public class AudioFeedback implements CollectionListener, DiagramTreeNodeListener { + + /** + * Construct an {@code AudioFeedback} object linked to a {@code DiagramTree}. + * + * @param tree the tree this instance is going to be linked to + */ + AudioFeedback(DiagramTree tree){ + resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + this.tree = tree; + } + + @Override + public void elementInserted(CollectionEvent e) { + DiagramEventSource source = (DiagramEventSource)e.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + final DiagramElement diagramElement = e.getDiagramElement(); + boolean isNode = diagramElement instanceof Node; + if(isNode){ + SoundFactory.getInstance().play( SoundEvent.OK ,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.input.node.ack"),diagramElement.spokenText())); + } + }); + }else{ + Edge edge = (Edge)diagramElement; + final StringBuilder builder = new StringBuilder(); + for(int i=0; i<edge.getNodesNum();i++){ + if(i == edge.getNodesNum()-1) + builder.append(edge.getNodeAt(i)+resources.getString("speech.input.edge.ack")); + else + builder.append(edge.getNodeAt(i)+ resources.getString("speech.input.edge.ack2")); + } + SoundFactory.getInstance().play( SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(builder.toString()); + } + }); + } + } + } + + @Override + public void elementTakenOut(CollectionEvent e) { + DiagramEventSource source = (DiagramEventSource)e.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + final DiagramElement element = e.getDiagramElement(); + SoundFactory.getInstance().play(SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.delete.element.ack"),element.spokenText(),tree.currentPathSpeech())); + } + }); + } + } + + @Override + public void elementChanged(ElementChangedEvent e) { + DiagramEventSource source = (DiagramEventSource)e.getSource(); + if(!source.isLocal() || source.type != DiagramEventSource.Type.TREE) + return; + String change = e.getChangeType(); + if("name".equals(change)){ + playOK(tree.currentPathSpeech()); + }else if ("property.add".equals(change)){ + PropertyChangeArgs args = (PropertyChangeArgs)e.getArguments(); + String propertyValue = ((Node)e.getDiagramElement()).getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex()); + playOK(MessageFormat.format(resources.getString("speech.input.property.ack"),propertyValue)); + }else if("property.set".equals(change)){ + playOK(tree.currentPathSpeech()); + }else if("property.remove".equals(change)){ + PropertyChangeArgs args = (PropertyChangeArgs)e.getArguments(); + playOK(MessageFormat.format(resources.getString("speech.deleted.property.ack"),args.getOldValue(),tree.currentPathSpeech())); + }else if("property.modifiers".equals(change)){ + playOK(tree.currentPathSpeech()); + }else if("arrowHead".equals(change)||"endLabel".equals(change)){ + playOK(tree.currentPathSpeech()); + } + } + + @Override + public void bookmarkAdded(DiagramTreeNodeEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + playOK(tree.currentPathSpeech()); + } + } + + @Override + public void bookmarkRemoved(DiagramTreeNodeEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + playOK(MessageFormat.format( + resources.getString("speech.delete.bookmark.ack"), + evt.getValue(), + tree.currentPathSpeech())); + } + } + + @Override + public void notesChanged(DiagramTreeNodeEvent evt) { + DiagramEventSource source = (DiagramEventSource)evt.getSource(); + if(source.isLocal() && source.type == DiagramEventSource.Type.TREE){ + playOK(tree.currentPathSpeech()); + } + } + + private void playOK(final String speech){ + SoundFactory.getInstance().play(SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(speech); + } + }); + } + + private ResourceBundle resources; + private DiagramTree tree; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/CCmIPopupMenu.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,442 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPopupMenu; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.network.AwarenessMessage; +import uk.ac.qmul.eecs.ccmi.network.Command; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * This class provides the two menus to handle nodes and edges on the visual graph. This class + * provides an abstract implementation common to both the node and edge menus. The specific + * implementations are internal static classes, inheriting from this class. + * + */ +@SuppressWarnings("serial") +public abstract class CCmIPopupMenu extends JPopupMenu { + /** + * This constructor is called by subclasses constructor. + * + * @param reference the element this menu refers to (it popped up by right-clicking on it) + * @param parentComponent the component where the menu is going to be displayed + * @param modelUpdater the model updater to make changed to {@code reference} + * @param selectedElements other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code reference}, being selected together with it. + */ + protected CCmIPopupMenu(DiagramElement reference, + Component parentComponent, DiagramModelUpdater modelUpdater, + Set<DiagramElement> selectedElements) { + super(); + this.modelUpdater = modelUpdater; + this.parentComponent = parentComponent; + this.reference = reference; + this.selectedElements = selectedElements; + } + + /** + * Returns the the element this menu refers to. + * @return the element this menu refers to. + */ + public DiagramElement getElement(){ + return reference; + } + + /** + * Add the a menu item to this menu. A menu item, once clicked on, will prompt the user for a new name + * for the referee element and will execute the update through the modelUpdater passed as argument + * to the constructor. + */ + protected void addNameMenuItem() { + /* add set name menu item */ + JMenuItem setNameMenuItem = new JMenuItem( + resources.getString("menu.set_name")); + setNameMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String type = (reference instanceof Node) ? "node" : "edge"; + if (!modelUpdater.getLock(reference, Lock.NAME, + new DiagramEventActionSource(DiagramEventSource.GRPH, + Command.Name.SET_NODE_NAME, reference.getId(),reference.getName()))) { + iLog("Could not get the lock on " + type + " for renaming", + DiagramElement.toLogString(reference)); + JOptionPane.showMessageDialog(parentComponent, + MessageFormat.format(resources + .getString("dialog.lock_failure.name"), + type)); + return; + } + iLog("open rename " + type + " dialog", + DiagramElement.toLogString(reference)); + String name = JOptionPane.showInputDialog(parentComponent, + MessageFormat.format( + resources.getString("dialog.input.name"), + reference.getName()), reference.getName()); + if (name == null) + iLog("cancel rename " + type + " dialog", + DiagramElement.toLogString(reference)); + else + /* node has been locked at selection time */ + modelUpdater.setName(reference, name.trim(), + DiagramEventSource.GRPH); + modelUpdater.yieldLock(reference, Lock.NAME, + new DiagramEventActionSource(DiagramEventSource.GRPH, + Command.Name.SET_NODE_NAME, reference.getId(),reference.getName())); + } + + }); + add(setNameMenuItem); + } + + /** + * Add the a delete item to this menu. A menu item, once clicked on, will prompt the user for a confirmation + * for the deletion of referee element and will execute the update through the modelUpdater passed as argument + * to the constructor. + */ + protected void addDeleteMenuItem() { + JMenuItem deleteMenuItem = new JMenuItem(resources.getString("menu.delete")); + deleteMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + /* create a new Set to maintain iterator consistency as elementTakenOut will change selectedItems */ + List<DiagramElement> workList = new ArrayList<DiagramElement>(selectedElements); + /* right click on an element with no selection involved */ + if(workList.isEmpty()){ + workList.add(reference); + /* right click on an element with other elements selected, thus we ignore the * + * currently selected elements and try to delete only the right clicked one. */ + }else if(!workList.contains(reference)){ + workList.clear(); + workList.add(reference); + }else{ + /* If the right clicked element selected together with other elements, try to * + * delete them all. First delete all edges and then all nodes to keep consistency. * + * We are deleting a bunch of objects and if a node is deleted before an edge * + * attached to it, then an exception will be triggered at the moment of edge * + * deletion because the edge will be already deleted as a result of the node deletion */ + Collections.sort(workList, new Comparator<DiagramElement>(){ + @Override + public int compare(DiagramElement e1,DiagramElement e2) { + boolean e1isEdge = e1 instanceof Edge; + boolean e2isEdge = e2 instanceof Edge; + if(e1isEdge && !e2isEdge){ + return -1; + } + if(!e1isEdge && e2isEdge){ + return 1; + } + return 0; + } + }); + } + + List<DiagramElement>alreadyLockedElements = new ArrayList<DiagramElement>(workList.size()); + /* check which, of the selected elements, can be deleted and which ones are currently held by * + * other clients. If an element is locked it's removed from the list and put into a separated set */ + for(Iterator<DiagramElement> itr=workList.iterator(); itr.hasNext();){ + DiagramElement selected = itr.next(); + boolean isNode = selected instanceof Node; + if(!modelUpdater.getLock(selected, + Lock.DELETE, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + selected.getId(),selected.getName()))){ + itr.remove(); + alreadyLockedElements.add(selected); + } + } + + /* all the elements are locked by other clients */ + if(workList.isEmpty()){ + iLog("Could not get lock on any selected element for deletion",""); + JOptionPane.showMessageDialog( + JOptionPane.getFrameForComponent(parentComponent), + alreadyLockedElements.size() == 1 ? // singular vs plural + resources.getString("dialog.lock_failure.delete") : + resources.getString("dialog.lock_failure.deletes")); + return; + } + + String warning = ""; + if(!alreadyLockedElements.isEmpty()){ + StringBuilder builder = new StringBuilder(resources.getString("dialog.lock_failure.deletes_warning")); + for(DiagramElement alreadyLocked : alreadyLockedElements) + builder.append(alreadyLocked.getName()).append(' '); + warning = builder.append('\n').toString(); + iLog("Could not get lock on some selected element for deletion",warning); + } + + iLog("open delete dialog",warning); + int answer = JOptionPane.showConfirmDialog( + JOptionPane.getFrameForComponent(parentComponent), + warning+resources.getString("dialog.confirm.deletions"), + resources.getString("dialog.confirm.title"), + SpeechOptionPane.YES_NO_OPTION); + if(answer == JOptionPane.YES_OPTION){ + /* the user chose to delete the elements, proceed (locks * + * will be automatically removed upon deletion by the server ) */ + for(DiagramElement selected : workList){ + modelUpdater.takeOutFromCollection(selected,DiagramEventSource.GRPH); + modelUpdater.sendAwarenessMessage( + AwarenessMessage.Name.STOP_A, + new DiagramEventActionSource(DiagramEventSource.TREE, + (selected instanceof Node) ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + selected.getId(),selected.getName()) + ); + } + }else{ + /* the user chose not to delete the elements, release the acquired locks */ + for(DiagramElement selected : workList){ + /* if it's a node all its attached edges were locked as well */ + /*if(selected instanceof Node){ DONE IN THE SERVER + Node n = (Node)selected; + for(int i=0; i<n.getEdgesNum();i++){ + modelUpdater.yieldLock(n.getEdgeAt(i), Lock.DELETE); + }}*/ + boolean isNode = selected instanceof Node; + modelUpdater.yieldLock(selected, + Lock.DELETE, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + selected.getId(),selected.getName())); + } + iLog("cancel delete node dialog",""); + } + } + }); + add(deleteMenuItem); + } + + /** + * Performs the log in the InteractionLog. + * @param action the action to log. + * @param args additional arguments to add to the log. + * + * @see uk.ac.qmul.eecs.ccmi.utils.InteractionLog + */ + protected void iLog(String action, String args) { + InteractionLog.log("GRAPH", action, args); + } + + /** + * + * A popup menu to perform changes (e.g. delete, rename etc.) to a node from the visual graph. + * + */ + public static class NodePopupMenu extends CCmIPopupMenu { + /** + * + * @param node the node this menu refers to. + * @param parentComponent the component where the menu is going to be displayed. + * @param modelUpdater the model updater used to make changed to {@code node}. + * @param selectedElements other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code node}, being selected together with it. + */ + NodePopupMenu(Node node, Component parentComponent, + DiagramModelUpdater modelUpdater, + Set<DiagramElement> selectedElements) { + super(node, parentComponent, modelUpdater, selectedElements); + addNameMenuItem(); + addPropertyMenuItem(); + addDeleteMenuItem(); + } + + private void addPropertyMenuItem() { + final Node nodeRef = (Node) reference; + /* if the node has no properties defined, then don't add the menu item */ + if(nodeRef.getProperties().isNull()) + return; + /* add set property menu item*/ + JMenuItem setPropertiesMenuItem = new JMenuItem(resources.getString("menu.set_properties")); + setPropertiesMenuItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent e) { + if(!modelUpdater.getLock(nodeRef, Lock.PROPERTIES, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName()))){ + iLog("Could not get the lock on node for properties",DiagramElement.toLogString(nodeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.properties")); + return; + } + iLog("open edit properties dialog",DiagramElement.toLogString(nodeRef)); + NodeProperties properties = PropertyEditorDialog.showDialog(JOptionPane.getFrameForComponent(parentComponent),nodeRef.getPropertiesCopy()); + if(properties == null){ // user clicked on cancel + iLog("cancel edit properties dialog",DiagramElement.toLogString(nodeRef)); + modelUpdater.yieldLock(nodeRef, Lock.PROPERTIES, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName())); + return; + } + if(!properties.isNull()) + modelUpdater.setProperties(nodeRef,properties,DiagramEventSource.GRPH); + modelUpdater.yieldLock(nodeRef, Lock.PROPERTIES,new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName())); + } + }); + add(setPropertiesMenuItem); + } + } + + /** + * A popup menu to perform changes (e.g. delete, rename etc.) to a edge from the visual graph. + */ + public static class EdgePopupMenu extends CCmIPopupMenu { + /** + * Constructs an {@code EdgePopupMenu} to perform changes to an edge from the visual diagram. + * This constructor is normally called when the user clicks in the neighbourhood of a node + * connected to this edge. the menu will then include items to change an end label or + * an arrow head. + * @param edge the edge this menu refers to. + * @param node one attached node some menu item will refer to. + * @param parentComponent the component where the menu is going to be displayed. + * @param modelUpdater the model updater used to make changed to {@code edge}. + * @param selectedElements other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code edge}, being selected together with it. + */ + public EdgePopupMenu( Edge edge, Node node, Component parentComponent, DiagramModelUpdater modelUpdater, + Set<DiagramElement> selectedElements){ + super(edge,parentComponent,modelUpdater,selectedElements); + addNameMenuItem(); + if(node != null){ + nodeRef = node; + Object[] arrowHeads = new Object[edge.getAvailableEndDescriptions().length + 1]; + for(int i=0;i<edge.getAvailableEndDescriptions().length;i++){ + arrowHeads[i] = edge.getAvailableEndDescriptions()[i].toString(); + } + arrowHeads[arrowHeads.length-1] = Edge.NO_ENDDESCRIPTION_STRING; + addEndMenuItems(arrowHeads); + } + addDeleteMenuItem(); + } + + /** + * Constructs an {@code EdgePopupMenu} to perform changes to an edge from the visual diagram. + * This constructor is normally called when the user clicks around the midpoint of the edge + * @param edge the edge this menu refers to. + * @param parentComponent the component where the menu is going to be displayed. + * @param modelUpdater the model updater used to make changed to {@code edge}. + * @param selectedElements other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code edge}, being selected together with it. + */ + public EdgePopupMenu( Edge edge, Component parentComponent, DiagramModelUpdater modelUpdater, + Set<DiagramElement> selectedElements){ + this(edge,null,parentComponent,modelUpdater,selectedElements); + } + + private void addEndMenuItems(final Object[] arrowHeads){ + final Edge edgeRef = (Edge)reference; + /* Label menu item */ + JMenuItem setLabelMenuItem = new JMenuItem(resources.getString("menu.set_label")); + setLabelMenuItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + if(!modelUpdater.getLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDLABEL,edgeRef.getId(),reference.getName()))){ + iLog("Could not get lock on edge for end label",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.end")); + return; + } + iLog("open edge label dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + String text = JOptionPane.showInputDialog(parentComponent,resources.getString("dialog.input.label")); + if(text != null) + modelUpdater.setEndLabel(edgeRef,nodeRef,text,DiagramEventSource.GRPH); + else + iLog("cancel edge label dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDLABEL,edgeRef.getId(),reference.getName())); + } + }); + add(setLabelMenuItem); + + if(arrowHeads.length > 1){ + /* arrow head menu item */ + JMenuItem selectArrowHeadMenuItem = new JMenuItem(resources.getString("menu.choose_arrow_head")); + selectArrowHeadMenuItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent e) { + if(!modelUpdater.getLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName()))){ + iLog("Could not get lock on edge for arrow head",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.end")); + return; + } + iLog("open select arrow head dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + String arrowHead = (String)JOptionPane.showInputDialog( + parentComponent, + resources.getString("dialog.input.arrow"), + resources.getString("dialog.input.arrow.title"), + JOptionPane.PLAIN_MESSAGE, + null, + arrowHeads, + arrowHeads); + if(arrowHead == null){ + iLog("cancel select arrow head dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef)); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName())); + return; + } + for(int i=0; i<edgeRef.getAvailableEndDescriptions().length;i++){ + if(edgeRef.getAvailableEndDescriptions()[i].toString().equals(arrowHead)){ + modelUpdater.setEndDescription(edgeRef, nodeRef, i,DiagramEventSource.GRPH); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName())); + return; + } + } + /* the user selected the none menu item */ + modelUpdater.setEndDescription(edgeRef,nodeRef, Edge.NO_END_DESCRIPTION_INDEX,DiagramEventSource.GRPH); + modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName())); + } + }); + add(selectArrowHeadMenuItem); + } + } + + private Node nodeRef; + } + + /** + * the model updater used to make changed to {@code reference}. + */ + protected DiagramModelUpdater modelUpdater; + /** + * the component where the menu is going to be displayed. + */ + protected Component parentComponent; + /** + * the element this menu refers to. + */ + protected DiagramElement reference; + /** + * other elements eventually selected on the graph, which are going + * to undergo the same changes as {@code reference}, being selected together with it. + */ + protected Set<DiagramElement> selectedElements; + private static ResourceBundle resources = ResourceBundle.getBundle(CCmIPopupMenu.class.getName()); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/CCmIPopupMenu.properties Tue Jul 08 16:28:59 2014 +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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Diagram.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,343 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.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; + +/** + * The {@code Diagram} class holds all the data needed for a representation of the diagram. It is used by component classes + * such as {@link GraphPanel} and {@link DiagramTree} to draw the diagram by accessing the diagram model or by + * {@link EditorTabbedPane} to assign a title to the tabs out of the diagram name. + * + */ +public abstract class Diagram implements Cloneable { + + /** + * Crates a new instance of a Diagram. The diagram created through this method is not shared with any peer via + * a server. + * @param name the name of the diagram. + * @param nodes an array of node prototypes. Nodes inserted by users in the diagram will be created by cloning these nodes. + * @param edges an array of edge prototypes. Edges inserted by users in the diagram will be created by cloning these edges. + * @param prototypePersistenceDelegate a delegate class to handle nodes and edges persistence. + * @return a new instance of {@code Diagram} + */ + public static Diagram newInstance(String name, Node[] nodes, Edge[] edges, PrototypePersistenceDelegate prototypePersistenceDelegate){ + return new LocalDiagram(name,nodes,edges,prototypePersistenceDelegate); + } + + /** + * Returns the name of the diagram. The name identifies the diagram uniquely in the editor. There cannot + * be two diagrams with the same name open at the same time. This makes things easier when sharing diagrams + * with other users via the network. + * + * @return the name of the diagram + */ + public abstract String getName(); + + /** + * Assign this diagram a new name. + * @param name the new name of the diagram + */ + public abstract void setName(String name); + + /** + * Returns an array with the node prototypes. Node prototypes are used when creating new node + * instances via the {@code clone()} method. + * + * @return an array of nodes + */ + public abstract Node[] getNodePrototypes(); + + /** + * Returns an array with the edge prototypes. Edge prototypes are used when creating new edge + * instances via the {@code clone()} method. + * + * @return an array of edges + */ + public abstract Edge[] getEdgePrototypes(); + + /** + * Returns the tree model of this diagram. Note that each diagram holds a {@code DiagramModel} + * which has two sub-models ({@code TreeModel} and {@code CollectionModel}). Changes on one + * sub-model will affect the other model as well. + * + * @return the tree model of this diagram + * + * @see uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel + */ + public abstract TreeModel<Node,Edge> getTreeModel(); + + /** + * Returns the collection model of this diagram. Note that each diagram holds a {@code DiagramModel} + * which has two sub-models ({@code TreeModel} and {@code CollectionModel}). Changes on one + * sub-model will affect the other model as well. + * + * @return the tree model of this diagram + * + * @see uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramModel + */ + public abstract CollectionModel<Node,Edge> getCollectionModel(); + + /** + * Returns the model updater of this diagram. The model updater is the delegate for all the + * update operations affecting the diagram model. + * + * @return the model updater for this diagram + */ + public abstract DiagramModelUpdater getModelUpdater(); + + /** + * Returns the label of the diagram. The label is slightly different from the name as it's the string + * appearing in the tabbed pane of the editor. It includes asterisk character at the end when the {@code DiagramModel} + * of this class has been changed and not yet saved on hard disk. + * + * @return a label for this diagram + */ + public abstract String getLabel(); + + /** + * Returns the delegates for this diagram for nodes and edges prototypes persistence. + * When saving a diagram to an xml file each node and edge of the prototypes is encoded + * in the xml file. Indeed the template of a diagram is made of of its prototypes. + * In the template is held the general attributes common to all the nodes and edges, like + * for instance the type of a node but not its current position. + * + * @return the PrototypePersistenceDelegate for this diagram + */ + public abstract PrototypePersistenceDelegate getPrototypePersistenceDelegate(); + + @Override + public Object clone(){ + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + public static class LocalDiagram extends Diagram { + + protected LocalDiagram(String name, Node[] nodes, Edge[] edges,PrototypePersistenceDelegate prototypePersistenceDelegate){ + this.name = name; + this.nodes = nodes; + this.edges = edges; + this.prototypePersistenceDelegate = prototypePersistenceDelegate; + diagramModel = new DiagramModel<Node,Edge>(nodes,edges); + innerModelUpdater = new InnerModelUpdater(); + } + + @Override + public String getName(){ + return name; + } + + @Override + public void setName(String name){ + this.name = name; + } + + @Override + public Node[] getNodePrototypes(){ + return nodes; + } + + @Override + public Edge[] getEdgePrototypes(){ + return edges; + } + + @Override + public TreeModel<Node,Edge> getTreeModel(){ + return diagramModel.getTreeModel(); + } + + @Override + public CollectionModel<Node,Edge> getCollectionModel(){ + return diagramModel.getDiagramCollection(); + } + + @Override + public String getLabel(){ + return name; + } + + @Override + public DiagramModelUpdater getModelUpdater(){ + return innerModelUpdater; + } + + @Override + public String toString(){ + return name; + } + + @Override + public PrototypePersistenceDelegate getPrototypePersistenceDelegate(){ + return prototypePersistenceDelegate; + } + + /** + * Creates a new {@code Diagram} by clonation. + */ + @Override + public Object clone(){ + LocalDiagram clone = (LocalDiagram)super.clone(); + clone.name = getName(); + clone.nodes = getNodePrototypes(); + clone.edges = getEdgePrototypes(); + /* constructor with no args makes just a dummy wrapper */ + clone.diagramModel = new DiagramModel<Node,Edge>(nodes,edges); + clone.innerModelUpdater = clone.new InnerModelUpdater(); + return clone; + } + + private DiagramModel<Node,Edge> diagramModel; + private InnerModelUpdater innerModelUpdater; + private PrototypePersistenceDelegate prototypePersistenceDelegate; + private String name; + private Node[] nodes; + private Edge[] edges; + + private class InnerModelUpdater implements DiagramModelUpdater { + + @Override + public boolean getLock(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(DiagramTreeNode treeNode, Lock lock, DiagramEventActionSource actionSource) {} + + @Override + public void sendAwarenessMessage(AwarenessMessage.Name awMsgName, Object source){} + + @Override + public void insertInCollection(DiagramElement element,DiagramEventSource source) { + if(element instanceof Node) + diagramModel.getDiagramCollection().insert((Node)element,source); + else + diagramModel.getDiagramCollection().insert((Edge)element,source); + } + + @Override + public void insertInTree(DiagramElement element) { + if(element instanceof Node) + diagramModel.getTreeModel().insertTreeNode((Node)element,DiagramEventSource.TREE); + else + diagramModel.getTreeModel().insertTreeNode((Edge)element,DiagramEventSource.TREE); + } + + @Override + public void takeOutFromCollection(DiagramElement element, DiagramEventSource source) { + diagramModel.getDiagramCollection().takeOut(element,source); + } + + @Override + public void takeOutFromTree(DiagramElement element) { + diagramModel.getTreeModel().takeTreeNodeOut(element,DiagramEventSource.TREE); + } + + @Override + public void setName(DiagramElement element, String name,DiagramEventSource source) { + element.setName(name,source); + } + + @Override + 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,DiagramEventSource source) { + node.setProperty(type, index, value,source); + } + + @Override + public void setProperties(Node node, NodeProperties properties,DiagramEventSource source) { + node.setProperties(properties,source); + } + + @Override + public void clearProperties(Node node,DiagramEventSource source) { + node.clearProperties(source); + } + + @Override + 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,DiagramEventSource source) { + node.removeProperty(type, index,source); + } + + @Override + public void setModifiers(Node node, String type, int index, + Set<Integer> modifiers,DiagramEventSource source) { + node.setModifierIndexes(type, index, modifiers,source); + } + + @Override + 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,DiagramEventSource source) { + edge.setEndDescription(node, index,source); + } + + @Override + 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,DiagramEventSource source) { + ge.startMove(p,source); + } + + @Override + public void bend(Edge edge, Point2D p,DiagramEventSource source) { + edge.bend(p,source); + } + + @Override + public void stopMove(GraphElement ge,DiagramEventSource source) { + ge.stopMove(source); + } + } + } + +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramEventSource.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,148 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +/** + * This class identifies the source of a diagram event, that is an event generated by an action + * in the diagram such as for instance a node insertion, deletion or renaming. The class carries informations + * about how the event was generated (from the tree, from the graph etc.) and if the event was + * generated by the local user or another user co-editing the diagram. In either case an id of the + * user is conveyed as well. An id is just a String each user assigns to themselves through a user + * interface panel. + */ +public class DiagramEventSource { + /** + * An enumeration of the different ways an event can be generated. NONE is for when the + * information is not relevant (as normally no event listener will be triggered by the event). + * PERS is for actions triggered when rebuilding a diagram from a ccmi file, so not as a consequence + * of a direct user action. + */ + public static enum Type{ + TREE, + GRPH, // graph + HAPT, // haptics + PERS, // persistence + NONE; + } + + /* constructor used only by the static event sources */ + private DiagramEventSource(Type type){ + this.type = type; + local = true; + } + + /** + * Creates a new DiagramEventSource out of a previous one (normally one of the static default sources). + * The type of the new object will be the same as the one passed as argument. Object created through + * this constructor are marked as non local + * + * @see #isLocal() + * @param eventSource an instance of this class + */ + public DiagramEventSource(DiagramEventSource eventSource){ + this.type = eventSource.type; + local = false; + } + + /** + * Returns true if the event is local, that is it's has been generated from an action of + * the local user and not from a message coming from the server. + * + * @return {@code true} if the event has been generated by the local user + */ + public boolean isLocal(){ + return local; + } + + /** + * Returns a copy of this event source that is marked as local. + * + * @return a local copy of this event source + * @see #isLocal() + */ + public DiagramEventSource getLocalSource(){ + return new DiagramEventSource(type); + } + + /** + * Returns the name of the diagram where the event happened, that has this + * object as source. + * + * @return the name of the diagram + */ + public String getDiagramName(){ + return diagramName; + } + + /** + * Sets the name of the diagram where the event happened, that has this + * object as source. + * + * @param diagramName the name of the diagram + */ + public void setDiagramName(String diagramName){ + this.diagramName = diagramName; + } + + /** + * The String representation of this object is the concatenation of the type + * and the ID. + * + * @return a String representing this object + */ + @Override + public String toString(){ + return (local ? ISLOCAL_CHAR : ISNOTLOCAL_CHAR )+type.toString(); + } + + /** + * Returns an instance of this class out of a string representation, such as + * returned by {@code toString()} + * @param s a String representation of a {@code DiagramEventSource} instance, such as + * returned by {@code toString()} + * @return a new instance of {@code DiagramEventSource} + */ + public static DiagramEventSource valueOf(String s){ + Type t = Type.valueOf(s.substring(1, 5)); + DiagramEventSource toReturn = new DiagramEventSource(t); + toReturn.local = (s.charAt(0) == ISLOCAL_CHAR) ? true : false; + return toReturn; + } + + private boolean local; + public final Type type; + private String diagramName; + private static char ISLOCAL_CHAR = 'L'; + private static char ISNOTLOCAL_CHAR = 'R'; + + /** Source for events triggered by the local user through the tree. These static sources + * are used when the diagram is not shared with any other user. When it is, a new DiagramEventSource + * will be created, which includes informations about the id and locality of the user who generated the event + */ + public static DiagramEventSource TREE = new DiagramEventSource(Type.TREE); + /** Source for events triggered by the local user through the graph */ + public static DiagramEventSource GRPH = new DiagramEventSource(Type.GRPH); + /** Source for events triggered by the local user through the haptic device */ + public static DiagramEventSource HAPT = new DiagramEventSource(Type.HAPT); + /** Source for events triggered by the local user when opening a file */ + public static DiagramEventSource PERS = new DiagramEventSource(Type.PERS); + /** Source for events not relevant to model listeners */ + public static DiagramEventSource NONE = new DiagramEventSource(Type.NONE); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramModelUpdater.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,260 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; +import java.util.Set; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.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; + +/** + * + * The DiagramModelUpdater class is used to make changes to the diagram model. The reason why + * changes are not made directly to the model is allowing the network-local diagram interchangeability. + * A NetDiagram differs from a local + * diagram only by its DiagramModelUpdater implementation. The rest of the operations are + * performed through the delegate local diagram which is passed as argument to the constructor. + * In this way a local diagram can be easily turned into a network diagram and vice versa. + * + * @see NetDiagram + */ +public interface DiagramModelUpdater { + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * Set the modifiers for a property of a node in of the {@code Diagram} holding this + * model updater. + * + * @param node the node whose properties whose modifiers are being + * @param type the type of the property whose modifiers are being set + * @param index the index of the property whose modifiers are being set + * @param modifiers the new modifiers indexes. the indexes refer to the modifiers type array. + * @param source the source of the {@code setModifiers} action. it can be used by collection listeners. + */ + public void setModifiers(Node node, String type, int index, Set<Integer> modifiers,DiagramEventSource source); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * Finishes off the motion of a graph element of the {@code Diagram} holding this + * model updater. + * + * @param ge the graph element being moved + * @param source the source of the {@code stopMove} action. it can be used by collection listeners. + */ + public void stopMove(GraphElement ge,DiagramEventSource source); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramPanel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,298 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.BorderLayout; +import java.io.IOException; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +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} + * a {@link GraphToolbar} and the {@code AwarenessPanel}. + * It's backed up by an instance of {@code Diagram}. + */ +@SuppressWarnings("serial") +public class DiagramPanel extends JPanel{ + + /** + * 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; + + 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 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 up delegate 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 the backing up delegate 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 {@code true} if the diagram is modified, {@code false} otherwise + */ + 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); +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramTree.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,784 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.io.InputStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.JOptionPane; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.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; +import uk.ac.qmul.eecs.ccmi.speech.Narrator; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + + +@SuppressWarnings("serial") +public class DiagramTree extends JTree { + /** + * Creates a new diagram tree. The model of this tree is set to the tree model + * held by the instance of {@code Diagram} passed as argument. The model is retrieved by a call + * to {@code getTreeModel()} on the diagram. + * <p> + * The tree doesn't allow interaction via the mouse. It can be navigated via the keyboard using + * the arrow keys. When a node is selected, cursoring up and down allows the user to go through + * all the sibling of the selected node. Cursoring right will expand the selected node (if it has children) + * and select its first child. Cursoring left will collapse a node and select its father. All the motions + * trigger a text to speech utterance (possibly accompanied by sound) about the new selected node. + * + * @param diagram a reference to the diagram holding the tree model for this tree. + */ + public DiagramTree(Diagram diagram){ + super(diagram.getTreeModel()); + this.diagram = diagram; + resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + + TreePath rootPath = new TreePath((DiagramTreeNode)diagram.getTreeModel().getRoot()); + setSelectionPath(rootPath); + collapsePath(rootPath); + selectedNodes = new ArrayList<Node>(); + setEditable(false); + getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + overwriteTreeKeystrokes(); + /* don't use the swing focus system as we provide one on our own */ + setFocusTraversalKeysEnabled(false); + getAccessibleContext().setAccessibleName("tree"); + } + + @SuppressWarnings("unchecked") + @Override + public TreeModel<Node,Edge> getModel(){ + return (TreeModel<Node,Edge>)super.getModel(); + } + + /** + * @see javax.swing.JTree#setModel(javax.swing.tree.TreeModel) + * + * @param newModel the new model for this tree + */ + public void setModel(TreeModel<Node,Edge> newModel){ + 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()); + } + + private void selectNode(final Node n){ + selectedNodes.add(n); + treeModel.valueForPathChanged(new TreePath(n.getPath()),n.getName()); + + SoundFactory.getInstance().play(SoundEvent.OK,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.node_selected"),n.spokenText())); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"node selected for edge",n.getName()); + } + + private void unselectNode(final Node n){ + selectedNodes.remove(n); + treeModel.valueForPathChanged(new TreePath(n.getPath()),n.getName()); + + SoundFactory.getInstance().play(SoundEvent.OK,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.node_unselected"),n.spokenText())); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"node unselected for edge",DiagramElement.toLogString(n)); + } + + /** + * Returns an array containing the references to all the nodes that have so far been selected + * for edge creation. A new array is created each time this method is called. + * + * @return an array of nodes + */ + public DiagramNode[] getSelectedNodes(){ + DiagramNode[] array = new DiagramNode[selectedNodes.size()]; + return selectedNodes.toArray(array); + } + + /** + * Makes all the nodes selected for edge creation unselected. This method should + * be called after an edge has been created, to get the user restart + * go over the selection process again. + * + */ + public void clearNodeSelections(){ + ArrayList<Node> tempList = new ArrayList<Node>(selectedNodes); + selectedNodes.clear(); + for(Node n : tempList){ + treeModel.valueForPathChanged(new TreePath(n.getPath()),n.getName()); + diagram.getModelUpdater().yieldLock(n, Lock.MUST_EXIST, 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(); + 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()); + else + return selectedPathTreeNode.spokenText(); + } + + /** + * 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(); + TreePath oldPath; + switch(jumpTo){ + case REFERENCE : + oldPath = getSelectionPath(); + DiagramTreeNode selectedTreeNode = (DiagramTreeNode)oldPath.getLastPathComponent(); + if(selectedTreeNode instanceof NodeReferenceMutableTreeNode){ + final Node n = (Node)((NodeReferenceMutableTreeNode)selectedTreeNode).getNode(); + setSelectionPath(new TreePath(n.getPath())); + SoundFactory.getInstance().play(SoundEvent.JUMP, new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(MessageFormat.format(resources.getString("speech.jump"),n.spokenText())); + } + }); + }else if(selectedTreeNode instanceof EdgeReferenceMutableTreeNode){ + final Edge e = (Edge)((EdgeReferenceMutableTreeNode)selectedTreeNode).getEdge(); + setSelectionPath(new TreePath(e.getPath())); + SoundFactory.getInstance().play(SoundEvent.JUMP,new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(MessageFormat.format(resources.getString("speech.jump"),e.spokenText())); + } + }); + } + /* assume the referee has only root in common with the reference and collapse everything up to the root (excluded) */ + collapseAll(selectedTreeNode, (DiagramTreeNode)selectedTreeNode.getPath()[1]); + break; + case ROOT : + final DiagramTreeNode from =(DiagramTreeNode)getSelectionPath().getLastPathComponent(); + setSelectionRow(0); + collapseAll(from,from.getRoot()); + SoundFactory.getInstance().play(SoundEvent.JUMP, new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(MessageFormat.format(resources.getString("speech.jump"),from.getRoot().spokenText())); + } + }); + break; +// case TYPE : // jumps to the ancestor type node of the current node, never used +// oldPath = getSelectionPath(); +// int index = 0; +// Object[] pathComponents = oldPath.getPath(); +// for(int i=0;i<pathComponents.length;i++){ +// if(pathComponents[i] instanceof TypeMutableTreeNode){ +// index=i; +// break; +// } +// } +// final DiagramTreeNode typeTreeNode = (DiagramTreeNode)oldPath.getPathComponent(index); +// setSelectionPath(new TreePath(typeTreeNode.getPath())); +// collapseAll((DiagramTreeNode)oldPath.getLastPathComponent(),typeTreeNode); +// SoundFactory.getInstance().play(SoundEvent.JUMP, new PlayerListener(){ +// @Override +// public void playEnded() { +// narrator.speak(MessageFormat.format(resources.getString("speech.jump"),typeTreeNode.spokenText())); +// } +// }); +// break; + case SELECTED_TYPE : + DiagramTreeNode root = (DiagramTreeNode)getModel().getRoot(); + Object[] types = new Object[root.getChildCount()]; + for(int i=0; i< root.getChildCount();i++) + types[i] = ((TypeMutableTreeNode)root.getChildAt(i)).getName();//not to spokenText as it would be too long + oldPath = getSelectionPath(); + /* initial value is the type node whose branch node is currently selected */ + /* it is set as the first choice in the selection dialog */ + Object initialValue; + if(oldPath.getPath().length < 2) + initialValue = types[0]; + else + initialValue = oldPath.getPathComponent(1);//type tree node + /* the selection from the OptionPane is the stering returned by getName() */ + InteractionLog.log(INTERACTIONLOG_SOURCE,"open select type to jump dialog",""); + final String selectedValue = (String)SpeechOptionPane.showSelectionDialog( + SpeechOptionPane.getFrameForComponent(this), + "select type to jump to", + types, + initialValue); + if(selectedValue == null){ + /* it speaks anyway as we set up the playerListener in the EditorFrame class. No need to use narrator then */ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + InteractionLog.log(INTERACTIONLOG_SOURCE,"cancel select type to jump dialog",""); + return; + } + /* we search in the root which type tree node has getName() equal to the selected one */ + TypeMutableTreeNode typeNode = null; + for(int i = 0; i< root.getChildCount(); i++){ + TypeMutableTreeNode temp = (TypeMutableTreeNode)root.getChildAt(i); + if(temp.getName().equals(selectedValue)){ + typeNode = temp; + break; + } + } + setSelectionPath(new TreePath(typeNode.getPath())); + if(oldPath.getPath().length >= 2) + collapseAll((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)); + } + }); + break; + case BOOKMARK : + TreeModel<Node,Edge> treeModel = getModel(); + + if(treeModel.getBookmarks().size() == 0){ + SoundFactory.getInstance().play(SoundEvent.ERROR ,new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(resources.getString("speech.no_bookmarks")); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"no bookmarks available",""); + return; + } + + String[] bookmarkArray = new String[treeModel.getBookmarks().size()]; + bookmarkArray = treeModel.getBookmarks().toArray(bookmarkArray); + + InteractionLog.log(INTERACTIONLOG_SOURCE,"open select bookmark dialog",""); + String bookmark = (String)SpeechOptionPane.showSelectionDialog( + JOptionPane.getFrameForComponent(this), + "Select bookmark", + bookmarkArray, + bookmarkArray[0] + ); + + if(bookmark != null){ + oldPath = getSelectionPath(); + DiagramTreeNode treeNode = treeModel.getBookmarkedTreeNode(bookmark); + collapseAll((DiagramTreeNode)oldPath.getLastPathComponent(), (DiagramTreeNode)treeModel.getRoot()); + setSelectionPath(new TreePath(treeNode.getPath())); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(SoundEvent.JUMP, new PlayerListener(){ + @Override + public void playEnded() { + narrator.speak(currentPathSpeech); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"bookmark selected",bookmark); + }else{ + /* it speaks anyway, as we set up the speech in the EditorFrame class. no need to use the narrator then */ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + InteractionLog.log(INTERACTIONLOG_SOURCE,"cancel select bookmark dialog",""); + return; + } + break; + + } + 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((DiagramTreeNode)oldPath.getLastPathComponent(),de); + setSelectionPath(new TreePath(de.getPath())); + SoundFactory.getInstance().play( SoundEvent.JUMP, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.jump"),de.spokenText())); + } + }); + } + + /* 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(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.isControlDown() + || e.isAltDown()) + super.processKeyEvent(e); + } + + private void overwriteTreeKeystrokes() { + /* overwrite the keys. up and down arrow are overwritten so that it loops when the top and the */ + /* bottom are reached rather than getting stuck */ + + /* Overwrite keystrokes up,down,left,right arrows and space, shift, ctrl */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0),"down"); + getActionMap().put("down", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + DiagramTreeNode treeNode = (DiagramTreeNode)getLastSelectedPathComponent(); + /* look if we've got a sibling node after (we are not at the bottom) */ + DiagramTreeNode nextTreeNode = treeNode.getNextSibling(); + SoundEvent loop = null; + if(nextTreeNode == null){ + DiagramTreeNode parent = treeNode.getParent(); + if(parent == null) /* root node, just stay there */ + nextTreeNode = treeNode; + else /* loop = go to first child of own parent */ + nextTreeNode = (DiagramTreeNode)parent.getFirstChild(); + loop = SoundEvent.LIST_BOTTOM_REACHED; + } + setSelectionPath(new TreePath(nextTreeNode.getPath())); + final InputStream finalSound = getTreeNodeSound(nextTreeNode); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + SoundFactory.getInstance().play(finalSound); + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move down",nextTreeNode.toString()); + }}); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0),"up"); + getActionMap().put("up", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + DiagramTreeNode treeNode = (DiagramTreeNode)getLastSelectedPathComponent(); + DiagramTreeNode previousTreeNode = treeNode.getPreviousSibling(); + SoundEvent loop = null; + if(previousTreeNode == null){ + DiagramTreeNode parent = treeNode.getParent(); + if(parent == null) /* root node */ + previousTreeNode = treeNode; + else + previousTreeNode = (DiagramTreeNode)parent.getLastChild(); + loop = SoundEvent.LIST_TOP_REACHED; + } + setSelectionPath(new TreePath(previousTreeNode.getPath())); + final InputStream finalSound = getTreeNodeSound(previousTreeNode); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + SoundFactory.getInstance().play(finalSound); + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move up",previousTreeNode.toString()); + }}); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0),"right"); + getActionMap().put("right", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + DiagramTreeNode treeNode = (DiagramTreeNode)path.getLastPathComponent(); + if(treeNode.isLeaf()){ + notifyBorderReached(treeNode); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move right","border reached"); + } + else{ + expandPath(path); + setSelectionPath(new TreePath(((DiagramTreeNode)treeNode.getFirstChild()).getPath())); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_EXPAND,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move right",((DiagramTreeNode)treeNode.getFirstChild()).toString()); + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0),"left"); + getActionMap().put("left", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + 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(((DiagramTreeNode)parent).getPath()); + setSelectionPath(newPath); + collapsePath(newPath); + final String currentPathSpeech = currentPathSpeech(); + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_COLLAPSE,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + InteractionLog.log(INTERACTIONLOG_SOURCE,"move left",((DiagramTreeNode)parent).toString()); + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,0),"space"); + getActionMap().put("space",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + NarratorFactory.getInstance().speak(currentPathSpeech()); + InteractionLog.log(INTERACTIONLOG_SOURCE,"info requested",""); + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,InputEvent.CTRL_DOWN_MASK),"ctrlspace"); + getActionMap().put("ctrlspace",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + /*//this code snippet reads out the whole path from the root to the selected node + * StringBuilder builder = new StringBuilder(); + * TreePath path = getSelectionPath(); + * for(Object o : path.getPath()){ + * builder.append(((DiagramModelTreeNode)o).spokenText()); + * builder.append(", "); + * } + * Narrator.getInstance().speak(builder.toString(), null); + */ + TreePath path = getSelectionPath(); + DiagramTreeNode treeNode = (DiagramTreeNode)path.getLastPathComponent(); + NarratorFactory.getInstance().speak(treeNode.detailedSpokenText()); + InteractionLog.log(INTERACTIONLOG_SOURCE,"detailed info requested",""); + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT,InputEvent.SHIFT_DOWN_MASK),"shift"); + getActionMap().put("shift",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + if(getSelectionPath().getLastPathComponent() instanceof Node){ + Node node = (Node)getSelectionPath().getLastPathComponent(); + if(selectedNodes.contains(node)){ + unselectNode(node); + diagram.getModelUpdater().yieldLock(node, Lock.MUST_EXIST,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), + resources.getString("dialog.lock_failure.must_exist"), + SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech()); + } + }); + return; + } + selectNode(node); + } + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"ctrldown"); + getActionMap().put("ctrldown",SpeechUtilities.getShutUpAction()); + + /* make the tree ignore the page up and page down keys */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,0),"none"); + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,0),"none"); + } + + private static InputStream getTreeNodeSound(DiagramTreeNode node){ + InputStream sound = null; + TreeNode[] newPath = node.getPath(); + if(!node.isRoot()){ + if(node.getChildCount() > 0){ // check whether it's the folder containing Node/Edge references + if(node.getChildAt(0) instanceof EdgeReferenceMutableTreeNode){ + sound = ((EdgeReferenceMutableTreeNode)node.getChildAt(0)).getEdge().getSound(); + }else{ + sound = ((TypeMutableTreeNode)newPath[1]).getPrototype().getSound(); + } + }else{ + if(node instanceof NodeReferenceMutableTreeNode){ + sound = ((NodeReferenceMutableTreeNode)node).getNode().getSound(); + }else if(node instanceof EdgeReferenceMutableTreeNode){ + sound = ((EdgeReferenceMutableTreeNode)node).getNode().getSound(); + }else{ + sound = ((TypeMutableTreeNode)newPath[1]).getPrototype().getSound(); + } + } + } + return sound; + } + + @Override + public void setSelectionPath(TreePath path){ + super.setSelectionPath(path); + scrollPathToVisible(path); + } + + private void notifyBorderReached(DiagramTreeNode n) { + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + + @Override + public String convertValueToText(Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus){ + StringBuilder builder = new StringBuilder(super.convertValueToText(value, selected, expanded, leaf, row, hasFocus)); + if(selectedNodes != null) + if(selectedNodes.contains(value)){ + builder.insert(0, SELECTED_NODE_MARK_BEGIN); + builder.append(SELECTED_NODE_MARK_END); + } + return builder.toString(); + } + + @Override + protected TreeModelListener createTreeModelListener(){ + return new DiagramTreeModelHandler(); + } + + + private List<Node> selectedNodes; + private Diagram diagram; + private ResourceBundle resources; + private static final char SELECTED_NODE_MARK_BEGIN = '<'; + private static final char SELECTED_NODE_MARK_END = '>'; + private static final String INTERACTIONLOG_SOURCE = "TREE"; + /** + * 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 + * by another peer on a network, and therefore not only as a response to a user's action. + * The algorithm works as follows (being A the tree node selected before any handler method M being called): + * + * if A ain't deleted as a result of M : do nothing + * if A's deleted as a result of M's execution : say A was the n-th sibling select the new n-th sibling + * or, if now the sibling nodes are less than n, select the one with highest index + * if no sibling nodes are still connected to the tree select the parent or the closest ancestor connected to the tree + */ + private class DiagramTreeModelHandler extends JTree.TreeModelHandler{ + + @Override + public void treeStructureChanged(final TreeModelEvent e) { + /* check first if what we're removing is in the selection path */ + TreePath path = e.getTreePath(); + boolean isInSelectionPath = false; + for(Object t : getSelectionPath().getPath()){ + if(path.getLastPathComponent() == t){ + isInSelectionPath = true; + break; + } + } + + if(isInSelectionPath){ + Object[] pathArray = getSelectionPath().getPath(); + DefaultMutableTreeNode root = (DefaultMutableTreeNode)getModel().getRoot(); + /* go along the path from the selected node to the root looking for a node * + * attached to the tree or with sibling nodes attached to the tree */ + for(int i=pathArray.length-1;i>=0;i--){ + 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 = (DiagramTreeNode)pathArray[i-1]; + if(parent.isNodeRelated(root) && parent.getChildCount() > 0){ + super.treeStructureChanged(e); + setSelectionPath(new TreePath(((DefaultMutableTreeNode)parent.getLastChild()).getPath())); + break; + } + } + } + }else + super.treeStructureChanged(e); + repaint(); + } + + @Override + public void treeNodesChanged(final TreeModelEvent e){ + TreePath path = getSelectionPath(); + super.treeNodesChanged(e); + setSelectionPath(path); + } + + @Override + public void treeNodesRemoved(final TreeModelEvent e){ + /* check first if what we're removing is in the selecton path */ + TreePath path = e.getTreePath(); + DiagramTreeNode removedTreeNode = (DiagramTreeNode)e.getChildren()[0]; + boolean isInSelectionPath = false; + for(Object t : getSelectionPath().getPath()){ + if(removedTreeNode == t){ + isInSelectionPath = true; + break; + } + } + 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){ + if(e.getSource() instanceof TreeModel){ + /* update the path only if the node has been removed from the tree or * + * if the currently selected tree node is going to be removed by this action * + * Need to call collapsePath only if the source of the deletion is the tree * + * as otherwise the selected node is always a leaf */ + collapsePath(path); + setSelectionPath(path); + }else{ + /* if we deleted from another source, then select the first non null node in the path * + * including the deleted node. E.g. if we're deleting the first child of a parent * + * and the node has siblings than the new first sibling will be selected */ + int limitForParentDeletion = (parentTreeNode instanceof Edge) ? 1 : 0; // an edge with one node is to be deleted + if(parentTreeNode.getChildCount() > limitForParentDeletion){ + setSelectionPath(new TreePath(((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())); + }else{ + /* the deleted node had no siblings, thus select the node checking from the parent up in the path to the first still existing node */ + Object[] pathArray = path.getPath(); + for(int i=path.getPathCount()-1;i>=0;i--){ + DiagramTreeNode itr = (DiagramTreeNode)pathArray[i]; + if(itr.getPath()[0] == getModel().getRoot()){ + TreePath newPath = new TreePath(itr.getPath()); + setSelectionPath(newPath); + collapsePath(newPath); + break; + } + } + } + } + }else + super.treeNodesRemoved(e); + + /* if the node was selected for edge creation, then remove it from the list */ + DiagramTreeNode removedNode = (DiagramTreeNode)e.getChildren()[0]; + selectedNodes.remove(removedNode); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Direction.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,144 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; + +/** + This class describes a direction in the 2D plane. + A direction is a vector of length 1 with an angle between 0 + (inclusive) and 360 degrees (exclusive). There is also + a degenerate direction of length 0. +*/ +public class Direction +{ + /** + Constructs a direction (normalized to length 1). + @param dx the x-value of the direction + @param dy the corresponding y-value of the direction + */ + public Direction(double dx, double dy) + { + x = dx; + y = dy; + double length = Math.sqrt(x * x + y * y); + if (length == 0) return; + x = x / length; + y = y / length; + } + + /** + Constructs a direction between two points + @param p the starting point + @param q the ending point + */ + public Direction(Point2D p, Point2D q) + { + this(q.getX() - p.getX(), + q.getY() - p.getY()); + } + + /** + * 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; + else + return false; + } + + /** + Turns this direction by an angle. + @param angle the angle in degrees + + @return a new object representing the turned direction + */ + public Direction turn(double angle){ + double a = Math.toRadians(angle); + return new Direction( + x * Math.cos(a) - y * Math.sin(a), + x * Math.sin(a) + y * Math.cos(a)); + } + + /** + Gets the x-component of this direction + @return the x-component (between -1 and 1) + */ + public double getX() { + return x; + } + + /** + Gets the y-component of this direction + @return the y-component (between -1 and 1) + */ + public double getY() { + return y; + } + + private boolean equals(double dx, double dy ){ + return ((x==dx)&&(y==dy)); + } + + private boolean equals(double dx, double dy , double delta){ + return ((Math.abs(x-dx)<delta)&&(Math.abs(y-dy)<delta)); + } + + @Override + public String toString(){ + return "("+x+","+y+")"; + } + + private double x; + private double y; + + private static final double DELTA = 0.05; + + public static final Direction NORTH = new Direction(0, -1); + public static final Direction SOUTH = new Direction(0, 1); + public static final Direction EAST = new Direction(1, 0); + public static final Direction WEST = new Direction(-1, 0); + public static final Direction NONE = new Direction(0, 0); + + public static Direction compute(Point2D p, Point2D q){ + double x,y; + x = p.getX() - q.getX(); + y = p.getY() - q.getY(); + double length = Math.sqrt(x * x + y * y); + if (length == 0) + return NONE; + x = x / length; + y = y / length; + if(NORTH.equals(x, y)) + return NORTH; + if(SOUTH.equals(x, y)) + return SOUTH; + if(EAST.equals(x, y)) + return EAST; + if(WEST.equals(x, y)) + return WEST; + return NONE; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,864 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.ResourceBundle; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramEdge; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; + +/** + * An edge in a graph. Edge objects are used in a GraphPanel to render a diagram edge visually. + * Edge objects are used in the tree representation of the diagram as well, as they're + * subclasses of {@link DiagramEdge} + * + */ +@SuppressWarnings("serial") +public abstract class Edge extends DiagramEdge implements GraphElement{ + + /** + * 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; + this.maxAttachedNodes = maxAttachedNodes; + this.style = style; + nodes = new ArrayList<Node>(); + } + + /* --- Methods inherited from DiagramEdge --- */ + @Override + public Node getNodeAt(int index){ + return nodes.get(index); + } + + @Override + public int getNodesNum(){ + return nodes.size(); + } + + @Override + public boolean removeNode(DiagramNode diagramNode, Object source){ + 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; + } + boolean nodeRemoved = nodes.remove(n); + /* for update in the haptic device */ + notifyChange(new ElementChangedEvent(this,n,"remove_node",source)); + return nodeRemoved; + } + } + + @Override + public void connect(List<DiagramNode> nodes) throws ConnectNodesException{ + assert(getNodesNum() == 0); + /* this is to eliminate duplicates */ + LinkedHashSet<Node> nodeSet = new LinkedHashSet<Node>(); + for(DiagramNode n : nodes) + nodeSet.add((Node)n); + + /* checks on connection consistency */ + if((nodeSet == null)||(nodeSet.size() < minAttachedNodes)) + throw new ConnectNodesException("You must select at least "+ minAttachedNodes + "nodes"); + if((nodeSet.size() > maxAttachedNodes)) + throw new ConnectNodesException("You must select at most " + maxAttachedNodes +" nodes"); + + points = new ArrayList<InnerPoint>(); + if(nodeSet.size() > 2){ + /* there are more than three nodes. compute the central inner point * + * which will connect all the nodes, as the middle points of the edge bound */ + Rectangle2D bounds = new Rectangle2D.Double(); + for(Node n : nodeSet){ + bounds.add(n.getBounds()); + } + InnerPoint p = new InnerPoint(); + p.translate(new Point2D.Double(0,0), bounds.getCenterX(), bounds.getCenterY(),DiagramEventSource.NONE); + p.neighbours.addAll(nodeSet); + points.add(p); + } + this.nodes.addAll(nodeSet); + + if(!points.isEmpty())// points is empty when the edge has two nodes only + masterInnerPoint = points.get(0); + } + + @Override + public abstract void draw(Graphics2D g2); + + @Override + public void translate(Point2D p, double dx, double dy, Object source){ + for(InnerPoint ip : points) + ip.translate(p, dx, dy,source); + } + + /** + * To be called before {@code bend}, determines from {@code downPoint} whether + * a line is going to be break into two lines (with the creation of a new inner point) + * or if the bending is determined by an already existing inner point being translated + */ + @Override + public void startMove(Point2D downPoint,Object source){ + this.downPoint = downPoint; + newInnerPoint = null; + for(InnerPoint itrPoint : points) + if(itrPoint.contains(downPoint)){ + /* clicked on an already existing EdgePoint */ + newInnerPoint = itrPoint; + newPointCreated = false; + } + if(newInnerPoint == null){ + /* no point under the click, create a new one */ + newInnerPoint = new InnerPoint(); + newInnerPoint.translate(downPoint, downPoint.getX() - newInnerPoint.getBounds().getCenterX(), + downPoint.getY() - newInnerPoint.getBounds().getCenterY(),DiagramEventSource.NONE); + newPointCreated = true; + /* this methods checks for segments of the edge which are aligned and makes a unique edge out of them */ + } + } + + /** + * If this edge is made out of several lines and two or more of them becomes + * aligned then they are blended in one single line and the inner point that was + * at the joint is removed. + * + * @param source the source of the {@code stopMove} action + */ + @Override + public void stopMove(Object source){ + for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){ + InnerPoint ePoint = pItr.next(); + if(ePoint.neighbours.size() > 2) + continue; + Rectangle2D startBounds = ePoint.getBounds(); + Rectangle2D endBounds = ePoint.neighbours.get(0).getBounds(); + Direction d1 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); + endBounds = ePoint.neighbours.get(1).getBounds(); + Direction d2 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); + if(d1.isParallel(d2)){ + InnerPoint p = null; + GraphElement q = null; + if(ePoint.neighbours.get(0) instanceof InnerPoint){ + p = (InnerPoint)ePoint.neighbours.get(0); + q = ePoint.neighbours.get(1); + p.neighbours.add(q); + p.neighbours.remove(ePoint); + } + if(ePoint.neighbours.get(1) instanceof InnerPoint){ + p = (InnerPoint)ePoint.neighbours.get(1); + q = ePoint.neighbours.get(0); + p.neighbours.add(q); + p.neighbours.remove(ePoint); + } + pItr.remove(); + } + } + notifyChange(new ElementChangedEvent(this,this,"stop_move",source)); + } + + @Override + public abstract Rectangle2D getBounds(); + + @Override + public Point2D getConnectionPoint(Direction d){return null;} + + @Override + public boolean contains(Point2D aPoint){ + if(points.isEmpty()){ + return fatStrokeContains (nodes.get(0), nodes.get(1), aPoint); + } + for(InnerPoint p : points){ + for(GraphElement ge : p.neighbours){ + if(fatStrokeContains(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); + closestNode = nodes.get(0); + } + if(line.getP2().distance(aPoint) < minDist){ + minDist = line.getP2().distance(aPoint); + closestNode = nodes.get(1); + } + return closestNode; + }else{ + for(InnerPoint p : points){ + for(GraphElement ge : p.getNeighbours()) + if(ge instanceof Node){ + Node n = (Node)ge; + Direction d = new Direction(p.getBounds().getCenterX() - n.getBounds().getCenterX(),p.getBounds().getCenterY() - n.getBounds().getCenterY()); + if(n.getConnectionPoint(d).distance(aPoint) < minDist){ + minDist = n.getConnectionPoint(d).distance(aPoint); + closestNode = n; + } + } + } + return closestNode; + } + } + + private void removePoint(InnerPoint p){ + /* we assume at this moment p has one neighbour only */ + InnerPoint neighbour = (InnerPoint)p.neighbours.get(0); + points.remove(p); + neighbour.neighbours.remove(p); + if(neighbour.neighbours.size() == 1) + removePoint(neighbour); + } + + /* checks if a point belongs to a shape with a margin of MAX_DIST*/ + private boolean fatStrokeContains(GraphElement ge1, GraphElement ge2, Point2D p){ + BasicStroke fatStroke = new BasicStroke((float) (2 * MAX_DIST)); + Line2D line = new Line2D.Double( + ge1.getBounds().getCenterX(), + ge1.getBounds().getCenterY(), + ge2.getBounds().getCenterX(), + ge2.getBounds().getCenterY()); + Shape fatPath = fatStroke.createStrokedShape(line); + return fatPath.contains(p); + } + + /** + * Returns a line connecting the centre of the graph elements passed as argument. + * + * @param start the first graph element + * @param end the second graph element + * + * @return a line connecting {@code start} and {@code end} + */ + protected Line2D.Double getSegment(GraphElement start, GraphElement end){ + Rectangle2D startBounds = start.getBounds(); + Rectangle2D endBounds = end.getBounds(); + Direction d = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); + return new Line2D.Double(start.getConnectionPoint(d), end.getConnectionPoint(d.turn(180))); + } + + /** + * Returns a list of the points where this edge and the nodes it connects come to a contact + * + * @return a list of points + */ + public List<Point2D> getConnectionPoints(){ + List<Point2D> list = new LinkedList<Point2D>(); + if(points.isEmpty()){ + Line2D line = getSegment(nodes.get(0),nodes.get(1)); + list.add(line.getP1()); + list.add(line.getP2()); + }else{ + for(InnerPoint p : points){ + for(GraphElement ge : p.neighbours) + if(ge instanceof Node){ + Direction d = new Direction(p.getBounds().getCenterX() - ge.getBounds().getCenterX(),p.getBounds().getCenterY() - ge.getBounds().getCenterY()); + list.add(((Node)ge).getConnectionPoint(d)); + } + } + } + return list; + } + + /** + * Gets the stipple pattern of this edge line style + * + * @see LineStyle#getStipplePattern() + * + * @return an int representing the stipple pattern of this edge + */ + public int getStipplePattern(){ + return getStyle().getStipplePattern(); + } + + /** + * Bends one of the lines forming this edge. + * + * When an line is bent, if the location where the bending happens is a line then a new + * inner point is created and the line is broken into two sub lines. If the location is an + * already existing inner point, then the point is translated. + * + * @param p the starting point of the bending + * @param source the source of the bending action + */ + 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 closest to where the new point lays */ + InnerPoint closestP1 = null; + GraphElement closestP2 = null; + double minDist = 0; + for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){ + InnerPoint ePoint = pItr.next(); + for(ListIterator<GraphElement> geItr = ePoint.neighbours.listIterator(); geItr.hasNext() && !found;){ + /* find the neighbour of the current edge point whose line the new point lays on */ + GraphElement next = geItr.next(); + double dist = Line2D.ptSegDist( + ePoint.getBounds().getCenterX(), + ePoint.getBounds().getCenterY(), + next.getBounds().getCenterX(), + next.getBounds().getCenterY(), + downPoint.getX(), + downPoint.getY() + ); + + if(closestP1 == null || dist < minDist){ + closestP1 = ePoint; + closestP2 = next; + minDist = dist; + continue; + } + } + } + + if(closestP2 instanceof InnerPoint ){ + /* remove current edge point from the neighbour's neighbours */ + ((InnerPoint)closestP2).neighbours.remove(closestP1); + /* add the new inner point */ + ((InnerPoint)closestP2).neighbours.add(newInnerPoint); + } + /*remove old neighbour from edge inner point neighbours */ + closestP1.neighbours.remove(closestP2); + newInnerPoint.neighbours.add(closestP1); + newInnerPoint.neighbours.add(closestP2); + /* add the new node to the list of EdgeNodes of this edge */ + points.add(newInnerPoint); + closestP1.neighbours.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); + }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); + } + } + } + + /** + * Encodes all the relevant data of this object in XML format. + * + * @param doc an XML document + * @param parent the parent XML element, where tag about this edge will be inserted + * @param nodes a list of all nodes of the diagram + */ + public void encode(Document doc, Element parent, List<Node> nodes){ + parent.setAttribute(PersistenceManager.TYPE,getType()); + parent.setAttribute(PersistenceManager.NAME, getName()); + parent.setAttribute(PersistenceManager.ID, String.valueOf(getId())); + + int numNodes = getNodesNum(); + if(numNodes > 0){ + Element nodesTag = doc.createElement(PersistenceManager.NODES); + parent.appendChild(nodesTag); + for(int i=0; i<numNodes;i++){ + Element nodeTag = doc.createElement(PersistenceManager.NODE); + nodeTag.setAttribute(PersistenceManager.ID, String.valueOf(getNodeAt(i).getId())); + nodeTag.setAttribute(PersistenceManager.LABEL, getEndLabel(getNodeAt(i))); + nodesTag.appendChild(nodeTag); + } + } + + if(!points.isEmpty()){ + Element pointsTag = doc.createElement(PersistenceManager.POINTS); + parent.appendChild(pointsTag); + for(InnerPoint point : points){ + Element pointTag = doc.createElement(PersistenceManager.POINT); + pointsTag.appendChild(pointTag); + pointTag.setAttribute(PersistenceManager.ID, String.valueOf(-(points.indexOf(point)+1))); + + Element positionTag = doc.createElement(PersistenceManager.POSITION); + pointTag.appendChild(positionTag); + Rectangle2D bounds = point.getBounds(); + positionTag.setAttribute(PersistenceManager.X,String.valueOf(bounds.getX())); + positionTag.setAttribute(PersistenceManager.Y,String.valueOf(bounds.getY())); + + Element neighboursTag = doc.createElement(PersistenceManager.NEIGHBOURS); + pointTag.appendChild(neighboursTag); + StringBuilder builder = new StringBuilder(); + for(GraphElement ge : point.getNeighbours()){ + if(ge instanceof Node){ + builder.append(((Node)ge).getId()); + }else{ + builder.append(-(points.indexOf(ge)+1)); + } + builder.append(" "); + } + builder.deleteCharAt(builder.length()-1); + neighboursTag.setTextContent(builder.toString()); + } + } + } + + + /** + * Decodes an edge from the XML representation. + * + * @see #encode(Document, Element, List) + * + * @param doc an XML document + * @param edgeTag the tag in the XML file related to this edge + * @param nodesId a map linking node ids in the XML file to the {@code Node} objects they represent + * @throws IOException if something goes wrong while reading the XML file + */ + public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{ + setName(edgeTag.getAttribute(PersistenceManager.NAME),DiagramEventSource.PERS); + if(getName().isEmpty()) + throw new IOException(); + try{ + setId(Integer.parseInt(edgeTag.getAttribute(PersistenceManager.ID))); + }catch(NumberFormatException nfe){ + throw new IOException(nfe); + } + + NodeList nodeList = edgeTag.getElementsByTagName(PersistenceManager.NODE); + List<DiagramNode> attachedNodes = new ArrayList<DiagramNode>(nodeList.getLength()); + List<String> labels = new ArrayList<String>(nodeList.getLength()); + for(int i=0; i<nodeList.getLength();i++){ + String id = ((Element)nodeList.item(i)).getAttribute(PersistenceManager.ID); + if(!nodesId.containsKey(id)) + throw new IOException(); + attachedNodes.add(nodesId.get(id)); + labels.add(((Element)nodeList.item(i)).getAttribute(PersistenceManager.LABEL)); + } + + try { + connect(attachedNodes); + } catch (ConnectNodesException e) { + throw new IOException(e); + } + + for(int i=0; i < labels.size(); i++){ + setEndLabel(attachedNodes.get(i), labels.get(i),DiagramEventSource.PERS); + } + + Map<String, InnerPoint> pointsId = new LinkedHashMap<String, InnerPoint>(); + NodeList pointTagList = edgeTag.getElementsByTagName(PersistenceManager.POINT); + + for(int i=0; i<pointTagList.getLength(); i++){ + InnerPoint point = new InnerPoint(); + Element pointTag = (Element)pointTagList.item(i); + String id = pointTag.getAttribute(PersistenceManager.ID); + /* id of property nodes must be a negative value */ + try{ + if(Integer.parseInt(id) >= 0) + throw new IOException(); + }catch(NumberFormatException nfe){ + throw new IOException(nfe); + } + + pointsId.put(id, point); + + if(pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0) == null) + throw new IOException(); + Element pointPositionTag = (Element)pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0); + double dx = 0,dy = 0; + try{ + dx = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.X)); + dy = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.Y)); + }catch(NumberFormatException nfe){ + throw new IOException(); + } + point.translate(new Point2D.Double(), dx, dy,DiagramEventSource.PERS); + } + + /* remove the master inner point eventually created by connect */ + /* we're going to replace it with the one in the XML file */ + points.clear(); + /* re do the cycle when all the points id have been Map-ped */ + for(int i=0; i<pointTagList.getLength(); i++){ + Element pointTag = (Element)pointTagList.item(i); + InnerPoint point = pointsId.get(pointTag.getAttribute(PersistenceManager.ID)); + + if(pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0) == null) + throw new IOException(); + Element pointNeighboursTag = (Element)pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0); + String pointNeighboursTagContent = pointNeighboursTag.getTextContent(); + String[] neighboursId = pointNeighboursTagContent.split(" "); + + for(String neighbourId : neighboursId){ + GraphElement ge = nodesId.get(neighbourId); + if(ge == null) // it ain't a node + ge = pointsId.get(neighbourId); + if(ge == null) + throw new IOException(); + point.neighbours.add(ge); + } + points.add(point); + if(i==0) + masterInnerPoint = point; + } + } + + /** + * Returns the minimum number of nodes that edge of this type can connect + * + * @return the minimum nodes for edges of this type + */ + public int getMinAttachedNodes(){ + return minAttachedNodes; + } + + /** + * Returns the maximum number of nodes that edge of this type can connect + * + * @return the maximum nodes for edges of this type + */ + public int getMaxAttachedNodes(){ + return maxAttachedNodes; + } + + /** + * Return the line style of this edge + * + * @return the line style of this edge + */ + public LineStyle getStyle(){ + return style; + } + + protected Point2D downPoint; + private List<Node> nodes; + + /* list containing the vertex of the edge which are not nodes */ + /** + * The list of the inner points of this edge + */ + protected List<InnerPoint> points; + /** + * For edges connecting more than two nodes, this is the central inner point where + * all the lines from the nodes join + */ + protected InnerPoint masterInnerPoint; + + private boolean newPointCreated; + private InnerPoint newInnerPoint; + private int minAttachedNodes; + private int maxAttachedNodes; + + private LineStyle style; + + + private static final double MAX_DIST = 5; + private static final Color POINT_COLOR = Color.GRAY; + /** + * The end description for an end that has no hand description set by the user + */ + public static final String NO_ENDDESCRIPTION_STRING = ResourceBundle.getBundle(EditorFrame.class.getName()).getString("no_arrow_string"); + + + /** + * When an edge's (straight) line is bent it breaks into two sub lines. At the point where this two + * sub lines join a square shaped point is painted. This class represent that point. Objects of this class + * are graph elements and the user can click on them and translate them along the graph. + * + */ + protected static class InnerPoint implements GraphElement{ + /** + * Creates a new inner point + */ + public InnerPoint(){ + bounds = new Rectangle2D.Double(0,0,DIM,DIM); + neighbours = new LinkedList<GraphElement>(); + } + + @Override + public void startMove(Point2D p, Object source){} + + @Override + public void stopMove(Object source){} + + @Override + public void draw(Graphics2D g2){ + Color oldColor = g2.getColor(); + g2.setColor(POINT_COLOR); + g2.fill(bounds); + g2.setColor(oldColor); + } + + @Override + public boolean contains(Point2D p){ + return bounds.contains(p); + } + + @Override + public Point2D getConnectionPoint(Direction d){ + return new Point2D.Double(bounds.getCenterX(),bounds.getCenterY()); + } + + @Override + public void translate(Point2D p, double dx, double dy, Object source){ + bounds.setFrame(bounds.getX() + dx, + bounds.getY() + dy, + bounds.getWidth(), + bounds.getHeight()); + } + + @Override + public Rectangle2D getBounds(){ + return (Rectangle2D)bounds.clone(); + } + + /** + * Neighbours are the graph elements (either nodes or other inner points) this inner point is + * directly connected to. Directly connected means there is a straight line from this node + * and the neighbour. + * + * @return a list of neighbours of this inner node + */ + public List<GraphElement> getNeighbours(){ + return neighbours; + } + + /** + * Returns true if this inner node and {@code ge} are neighbours. + * + * @see #getNeighbours() + * + * @param ge the graph element to be tested + * @return {@code true} if {@code ge} is a neighbour of this graph element, {@code false} otherwise + * + */ + public boolean hasNeighbour(GraphElement ge){ + return neighbours.contains(ge); + } + + @Override + public String toString(){ + return "EdgePoint: "+bounds.getCenterX()+"-"+bounds.getCenterY(); + } + + private Rectangle2D bounds; + private List<GraphElement> neighbours; + private static final int DIM = 7; + } + + /** + * 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]; + ys = new double[size]; + adjMatrix = new BitSet[size]; + for(int i=0; i<size; i++){ + 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[]; + /** + * 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 + pr.xs[0] = nodes.get(0).getBounds().getCenterX(); + pr.ys[0] = nodes.get(0).getBounds().getCenterY(); + pr.xs[1] = nodes.get(1).getBounds().getCenterX(); + pr.ys[1] = nodes.get(1).getBounds().getCenterY(); + // we only need one edge, else it would be painted twice + pr.adjMatrix[0].set(1); + pr.nodeStart = 0; + }else{ + //[ point 1, point 2, point 3, ... , point n, node, 1 node 2, ... , node n ] + int pSize = points.size(); + pr.nodeStart = pSize; // the first node starts after the points + for(int i=0; i<pSize;i++){ + pr.xs[i] = points.get(i).getBounds().getCenterX(); + pr.ys[i] = points.get(i).getBounds().getCenterY(); + for(GraphElement ge : points.get(i).neighbours){ + if(ge instanceof InnerPoint) + pr.adjMatrix[i].set(points.indexOf(ge)); + else //Node + pr.adjMatrix[i].set(pSize+nodes.indexOf(ge)); + } + } + /* set the coordinates of the nodes, no adj matrix needed as the inner points are enough */ + for(int i=0 ; i<nodes.size(); i++){ + pr.xs[pSize+i] = nodes.get(i).getBounds().getCenterX(); + pr.ys[pSize+i] = nodes.get(i).getBounds().getCenterY(); + } + } + return pr; + } + +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,2465 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.KeyboardFocusManager; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.channels.SocketChannel; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JTree; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.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; +import uk.ac.qmul.eecs.ccmi.network.Server; +import uk.ac.qmul.eecs.ccmi.pdsupport.PdDiagram; +import uk.ac.qmul.eecs.ccmi.pdsupport.PdPersistenceManager; +import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.Narrator; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.ExceptionHandler; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/** + * 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 { + /** + * 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 the template editors for this instance of the program + * @param additionalTemplate additional diagram templates. An entry will be created in {@code File->New Diagram} + * for each template + */ + public EditorFrame(Haptics haptics, File[] templateFiles, String backupDirPath, TemplateEditor[] templateEditors, Diagram[] additionalTemplate){ + 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(); + + URL url = getClass().getResource("ccmi_favicon.gif"); + setIconImage(new ImageIcon(url).getImage()); + changeLookAndFeel(preferences.get("laf", null)); + + recentFiles = new ArrayList<String>(); + File lastDir = new File("."); + String recent = preferences.get("recent", "").trim(); + if (recent.length() > 0){ + recentFiles.addAll(Arrays.asList(recent.split("[|]"))); + lastDir = new File(recentFiles.get(0)).getParentFile(); + } + fileService = new FileService.ChooserService(lastDir); + + /* set up extensions */ + defaultExtension = resources.getString("files.extension"); + extensionFilter = new ExtensionFilter( + resources.getString("files.name"), + new String[] { defaultExtension }); + exportFilter = new ExtensionFilter( + resources.getString("files.image.name"), + resources.getString("files.image.extension")); + + /* start building the GUI */ + editorTabbedPane = new EditorTabbedPane(this); + setContentPane(editorTabbedPane); + + + setTitle(resources.getString("app.name")); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + + int screenWidth = (int)screenSize.getWidth(); + int screenHeight = (int)screenSize.getHeight(); + + setLocation(screenWidth / 16, screenHeight / 16); + editorTabbedPane.setPreferredSize(new Dimension( + screenWidth * 5 / 8, screenHeight * 5 / 8)); + + /* install the player listener (a narrator speech) for CANCEL, EMPTY and MESSAGE ok event. They are * + * not depending on what the user is doing and the speech is therefore always the same. We only need * + * to set the listener once and it will do the job each time the sound is triggered */ + SoundFactory.getInstance().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"),"")); + } + } + }, 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); + + /* setup listeners */ + initListeners(); + /* set up menus */ + existingTemplateNames = new ArrayList<String>(10); + existingTemplates = new ArrayList<Diagram>(10); + int extensionLength = resources.getString("template.extension").length(); + for(File file : templateFiles){ + existingTemplateNames.add(file.getName().substring(0, file.getName().length()-extensionLength)); + } + initMenu(templateEditors); + /* read template files. this call must be placed after menu creation as it adds menu items to file->new-> */ + boolean someTemplateFilesNotRead = readTemplateFiles(templateFiles); + + for(Diagram additionalDiagram : additionalTemplate){ + addDiagramType(additionalDiagram); + } + + /* become visible */ + pack(); + setVisible(true); + /* if some templates were not read successfully, warn the user with a message */ + if(someTemplateFilesNotRead){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.error.filesnotread"), SpeechOptionPane.WARNING_MESSAGE); + } + } + + private void initListeners(){ + /* window closing */ + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter(){ + @Override + public void windowClosing(WindowEvent event){ + exit(); + } + @Override + public void windowOpened(WindowEvent e) { + // bring the window to front, else the openGL window would have higher priority + e.getWindow().toFront(); + } + }); + + addWindowFocusListener(new WindowFocusListener(){ + @Override + public void windowGainedFocus(WindowEvent evt) { + if(evt.getOppositeWindow() == null) + NarratorFactory.getInstance().speak(resources.getString("window.focus")); + } + + @Override + public void windowLostFocus(WindowEvent evt) { + if(evt.getOppositeWindow() == null) + NarratorFactory.getInstance().speak(resources.getString("window.unfocus")); + } + }); + + /* set up listeners reacting to change of selection in the tree and tab */ + tabChangeListener = new ChangeListener(){ + @Override + public void stateChanged(ChangeEvent evt) { + DiagramPanel diagramPanel = getActiveTab(); + if (diagramPanel != null){ + /* give the focus to the Content Pane, 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()))); + + // 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); + + treeSelectionListener = new TreeSelectionListener(){ + @Override + public void valueChanged(TreeSelectionEvent evt) { + treeEnabledMenuUpdate(evt.getPath()); + } + }; + + netLocalDiagramExceptionHandler = new ExceptionHandler(){ + @Override + public void handleException(Exception e) { + SwingUtilities.invokeLater(new Runnable(){ + @Override + public void run() { + SpeechOptionPane.showMessageDialog(EditorFrame.this, resources.getString("dialog.error.local_server")); + } + }); + } + }; + } + + private void initMenu(TemplateEditor[] templateEditors){ + ResourceFactory factory = new ResourceFactory(resources); + + JMenuBar menuBar = SpeechMenuFactory.getMenuBar(); + setJMenuBar(menuBar); + + /* --- FILE MENU --- */ + JMenu fileMenu = factory.createMenu("file"); + menuBar.add(fileMenu); + + /* menu items and listener added by addDiagramType function */ + newMenu = factory.createMenu("file.new"); + fileMenu.add(newMenu); + + JMenuItem fileOpenItem = factory.createMenuItem( + "file.open", this, "openFile"); + fileMenu.add(fileOpenItem); + + recentFilesMenu = factory.createMenu("file.recent"); + buildRecentFilesMenu(); + fileMenu.add(recentFilesMenu); + + fileSaveItem = factory.createMenuItem("file.save", this, "saveFile"); + fileMenu.add(fileSaveItem); + + fileSaveAsItem = factory.createMenuItem("file.save_as", this, "saveFileAs"); + fileMenu.add(fileSaveAsItem); + + fileSaveCopyItem = factory.createMenuItem("file.save_copy", this, "saveCopy"); + fileMenu.add(fileSaveCopyItem); + + 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); + + /* --- EDIT MENU --- */ + JMenu editMenu = factory.createMenu("edit"); + menuBar.add(editMenu); + + locateMenuItem = factory.createMenuItem("edit.locate", this,"locate"); + editMenu.add(locateMenuItem); + + highlightMenuItem = factory.createMenuItem("edit.highlight", this, "hHighlight"); + highlightMenuItem.setEnabled(false); + editMenu.add(highlightMenuItem); + + jumpMenuItem = factory.createMenuItem("edit.jump",this,"jump"); + editMenu.add(jumpMenuItem); + + insertMenuItem = factory.createMenuItem("edit.insert", this, "insert"); + editMenu.add(insertMenuItem); + + deleteMenuItem = factory.createMenuItem("edit.delete",this,"delete"); + editMenu.add(deleteMenuItem); + + renameMenuItem = factory.createMenuItem("edit.rename",this,"rename"); + editMenu.add(renameMenuItem); + + 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 --- */ + if(templateEditors.length > 0){ + JMenu templateMenu = factory.createMenu("template"); + menuBar.add(templateMenu); + + for(final TemplateEditor templateEditor : templateEditors){ + JMenuItem newDiagramItem = SpeechMenuFactory.getMenuItem(templateEditor.getLabelForNew()); + newDiagramItem.addActionListener(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(newDiagramItem); + + JMenuItem editDiagramItem = SpeechMenuFactory.getMenuItem(templateEditor.getLabelForEdit()); + editDiagramItem.addActionListener(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); + } + } + ); + templateMenu.add(editDiagramItem); + } + } + + /* --- 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); + + /* show haptic window menu item only unless it's a actual haptic device thread * + * that has its own window run by a native dll */ + if(!HapticsFactory.getInstance().isAlive()){ + preferencesMenu.add(factory.createCheckBoxMenuItem("preferences.show_haptics", new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt){ + JCheckBoxMenuItem menuItem = (JCheckBoxMenuItem) evt.getSource(); + haptics.setVisible(menuItem.isSelected()); + NarratorFactory.getInstance().speakWholeText(resources.getString( + menuItem.isSelected() ? "speech.haptic_window_open" : "speech.haptic_window_close")); + } + })); + } + + preferencesMenu.add(factory.createMenuItem("preferences.change_haptics", new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt){ + String [] hapticDevices = { + HapticsFactory.PHANTOM_ID, + HapticsFactory.FALCON_ID, + HapticsFactory.TABLET_ID}; + String selection = (String)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + resources.getString("dialog.input.haptics.select"), + hapticDevices, + hapticDevices[0]); + if(selection == null) + return; + preferences.put("haptic_device", selection); + SpeechOptionPane.showMessageDialog(EditorFrame.this, + MessageFormat.format(resources.getString("dialog.feedback.haptic_init"),selection), + SpeechOptionPane.WARNING_MESSAGE); + } + })); + + // 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())); + } + }); + + JMenuItem enableLogMenuItem = factory.createCheckBoxMenuItem("preferences.enable_log", new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + JCheckBoxMenuItem menuItem = (JCheckBoxMenuItem) evt.getSource(); + PreferencesService preferences = PreferencesService.getInstance(); + preferences.put("enable_log", Boolean.toString(menuItem.isSelected())); + if(menuItem.isSelected()){ + try{ + InteractionLog.enable(preferences.get("dir.log", "")); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(EditorFrame.this, resources.getString("dialog.error.log_enable")); + } + }else{ + InteractionLog.disable(); + } + } + }); + + /* temporarily mute the narrator to select the menus without triggering a speech */ + NarratorFactory.getInstance().setMuted(true,Narrator.FIRST_VOICE); + fileChooserMenuItem.setSelected(Boolean.parseBoolean(PreferencesService.getInstance().get("use_accessible_filechooser", "true"))); + enableLogMenuItem.setSelected(Boolean.parseBoolean(PreferencesService.getInstance().get("enable_log", "false"))); + NarratorFactory.getInstance().setMuted(false,Narrator.FIRST_VOICE); + preferencesMenu.add(fileChooserMenuItem); + preferencesMenu.add(enableLogMenuItem); + + + /* --- 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 = UIManager.getSystemLookAndFeelClassName(); + try{ + UIManager.setLookAndFeel(lafName); + SwingUtilities.updateComponentTreeUI(EditorFrame.this); + } + catch (ClassNotFoundException ex) {} + catch (InstantiationException ex) {} + catch (IllegalAccessException ex) {} + catch (UnsupportedLookAndFeelException ex) {} + } + + /** + * Adds a file name to the "recent files" list and rebuilds the "recent files" menu. + * @param newFile the file name to add + */ + private void addRecentFile(final String newFile){ + recentFiles.remove(newFile); + if (newFile == null || newFile.equals("")) return; + recentFiles.add(0, newFile); + buildRecentFilesMenu(); + } + + /* speaks out the selected tree node if the tree is focused or the tab label if the tab is focused */ + private void speakFocusedComponent(String message){ + message = (message == null) ? "" : message+"; ";//add a dot to pause the TTS + DiagramPanel dPanel = getActiveTab(); + if(dPanel != null){ + String focusedComponent = null; + if(KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() instanceof JTree) + focusedComponent = dPanel.getTree().currentPathSpeech(); + else + focusedComponent = MessageFormat.format(resources.getString("window.tab"),editorTabbedPane.getComponentTabTitle(dPanel)); + NarratorFactory.getInstance().speak(message+focusedComponent); + } + } + /** + * Rebuilds the "recent files" menu. + */ + private void buildRecentFilesMenu(){ + recentFilesMenu.removeAll(); + for (int i = 0; i < recentFiles.size(); i++){ + final String file = recentFiles.get(i); + String name = new File(file).getName(); + JMenuItem item = SpeechMenuFactory.getMenuItem(name); + item.setToolTipText(file); + recentFilesMenu.add(item); + item.addActionListener(new + ActionListener(){ + public void actionPerformed(ActionEvent event){ + try { + FileService.Open open = new FileService.DirectService().open(new File(((JMenuItem)event.getSource()).getToolTipText())); + InputStream in = open.getInputStream(); + String path = open.getPath(); + for(int i=0; i<editorTabbedPane.getTabCount();i++){ + if(path.equals(editorTabbedPane.getToolTipTextAt(i))){ + editorTabbedPane.setSelectedIndex(i); + return; + } + } + Diagram diagram = path.endsWith(PdPersistenceManager.PD_EXTENSION) ? + PdPersistenceManager.getInstance().decodeDiagramInstance(in) : + PersistenceManager.decodeDiagramInstance(in); + addTab(open.getPath(), diagram); + } catch (IOException exception) { + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + exception.getLocalizedMessage()); + } + } + }); + } + } + + /** + Asks the user to open a graph file. + */ + public void openFile(){ + InputStream in = null; + try{ + FileService.Open open = fileService.open(null,null, extensionFilter,this); + in = open.getInputStream(); + if(in != null){ // open.getInputStream() == null -> user clicked on cancel + String path = open.getPath(); + int index = editorTabbedPane.getPathTabIndex(path); + if(index != -1){ //diagram is already open + editorTabbedPane.setSelectedIndex(index); + speakFocusedComponent(""); + return; + } + /* every opened diagram must have a unique name */ + if(editorTabbedPane.getDiagramNameTabIndex(open.getName()) != -1) + throw new IOException(resources.getString("dialog.error.same_file_name")); + iLog("START READ LOCAL DIAGRAM "+open.getName()); + Diagram diagram = path.endsWith(PdPersistenceManager.PD_EXTENSION) ? + PdPersistenceManager.getInstance().decodeDiagramInstance(in) : + 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();} + } + } + + /** + * Close a diagram tab. If the diagram has not been saved the user will be + * asked if they want to save the diagram. + */ + 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); + } + + /** + * Saves the currently open tab diagram into a file. If the diagram has no file path associated + * (it has never been saved before), {@link #saveFileAs()} is called. + * + * @return {@code true} if the file is successfully saved, or {@code false} otherwise. + */ + public boolean saveFile(){ + DiagramPanel diagramPanel = getActiveTab(); + if (diagramPanel == null) // no tabs open + return false; + String filePath = diagramPanel.getFilePath(); + if (filePath == null) { + return saveFileAs(); + } + + OutputStream out = null; + try{ + File file = new File(filePath); + out = new BufferedOutputStream(new FileOutputStream(file)); + Diagram d = diagramPanel.getDiagram(); + if(d instanceof PdDiagram){ + PdPersistenceManager.getInstance().encodeDiagramInstance(d, out); + }else { + 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 */ } + } + } + + /** + * Prompts the user with a dialog for saving a diagram on disk. The current diagram is not affected + * by this call and it keeps to be displayed in the editor. + * + * @return {@code true} if the file is successfully saved, or {@code false} otherwise. + */ + public boolean saveCopy(){ + DiagramPanel diagramPanel = getActiveTab(); + if (diagramPanel == null) // no tabs open + return false; + OutputStream out = null; + FileService.Save save; + try { + save = fileService.save( + PreferencesService.getInstance().get("dir.diagrams", "."), + diagramPanel.getFilePath(), //default file to save to + extensionFilter, + null, + defaultExtension, + null); + out = save.getOutputStream(); + if (out == null) /* user didn't select any file for saving */ + return false; + if(diagramPanel.getDiagram() instanceof PdDiagram){ + PdPersistenceManager.getInstance().encodeDiagramInstance(diagramPanel.getDiagram(),save.getName(), out); + }else{ + PersistenceManager.encodeDiagramInstance(diagramPanel.getDiagram(),save.getName(), out); + } + speakFocusedComponent(resources.getString("dialog.file_saved")); + return true; + } catch (IOException ioe) { + SpeechOptionPane.showMessageDialog(editorTabbedPane,ioe.getLocalizedMessage()); + return false; + } finally { + if(out != null) + try{out.close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + + /** + * Saves the current diagram as a new file. The user is prompter with a {@code FileChooser} + * to chose a file to save the diagram to. + * + * @return {@code true} if the file is successfully saved, or {@code false} otherwise. + */ + public boolean saveFileAs() { + DiagramPanel diagramPanel = getActiveTab(); + if (diagramPanel == null) // no tabs open + return false; + String oldName = diagramPanel.getDiagram().getName(); + OutputStream out = null; + try { + String[] currentTabs = new String[editorTabbedPane.getTabCount()]; + for(int i=0; i<editorTabbedPane.getTabCount();i++){ + currentTabs[i] = editorTabbedPane.getTitleAt(i); + } + FileService.Save save = fileService.save( + PreferencesService.getInstance().get("dir.diagrams", "."), + diagramPanel.getFilePath(), + extensionFilter, + null, + defaultExtension, + currentTabs); + out = save.getOutputStream(); + if (out == null) /* user didn't select any file for saving */ + return false; + + if(diagramPanel.getDiagram() instanceof PdDiagram){ + PdPersistenceManager.getInstance().encodeDiagramInstance(diagramPanel.getDiagram(),save.getName(), out); + }else{ + PersistenceManager.encodeDiagramInstance(diagramPanel.getDiagram(),save.getName(), out); + } + + /* update the diagram and the diageam panel, after the saving */ + diagramPanel.getDiagram().setName(save.getName()); + diagramPanel.setFilePath(save.getPath()); + diagramPanel.setModified(false); + speakFocusedComponent(resources.getString("dialog.file_saved")); + /* update the haptics, remove first the diagram that is going o be renamed * + * and then re-add the diagram under the new name (avoids haptics * + * IllegalArgumentException when switching tab) */ + haptics.removeDiagram(oldName, null); + haptics.addNewDiagram(diagramPanel.getDiagram().getName()); + for(Node n : diagramPanel.getDiagram().getCollectionModel().getNodes()) + haptics.addNode(n.getBounds().getCenterX(), n.getBounds().getCenterY(), System.identityHashCode(n),null); + for(Edge e : diagramPanel.getDiagram().getCollectionModel().getEdges()){ + Edge.PointRepresentation pr = e.getPointRepresentation(); + haptics.addEdge(System.identityHashCode(e),pr.xs,pr.ys,pr.adjMatrix,pr.nodeStart,e.getStipplePattern(),e.getNameLine(),null); + } + return true; + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(editorTabbedPane,ioe.getLocalizedMessage()); + return false; + }finally{ + if(out != null) + try{out.close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + + /** + Exits the program if no graphs have been modified + or if the user agrees to abandon modified graphs. + */ + public void exit(){ + /* check first whether there are modified diagrams */ + int diagramsToSave = 0; + for(int i=0; i<editorTabbedPane.getTabCount();i++){ + DiagramPanel dPanel = editorTabbedPane.getComponentAt(i); + if(dPanel.isModified()||dPanel.getFilePath() == null){ + diagramsToSave++; + } + } + + if(diagramsToSave > 0){ + int answer = SpeechOptionPane.showConfirmDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.confirm.exit"), diagramsToSave), + SpeechOptionPane.YES_NO_OPTION); + // if the doesn't want to save changes, veto the close + if(answer != SpeechOptionPane.NO_OPTION){ + if(answer == SpeechOptionPane.YES_OPTION){ // user clicked on yes button we just get them back to the editor + speakFocusedComponent(""); + }else{// user pressed the ESC button + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + return; + } + } + + NarratorFactory.getInstance().dispose(); + SoundFactory.getInstance().dispose(); + haptics.dispose(); + if(server != null) + server.shutdown(); + if(clientConnectionManager != null) + clientConnectionManager.shutdown(); + BroadcastFilter broadcastFilter = BroadcastFilter.getInstance(); + if(broadcastFilter != null) + broadcastFilter.saveProperties(this); + DisplayFilter displayFilter = DisplayFilter.getInstance(); + if(displayFilter != null) + displayFilter.saveProperties(this); + + while(haptics.isAlive()){/* wait */} + /* closes the logger's handlers */ + iLog("PROGRAM EXIT"); + InteractionLog.dispose(); + + savePreferences(); + System.exit(0); + } + + /** + * Changes the selection path of the diagram tree to a specific destination. + * The user with a selection dialog to choose the destination from the following + * choices : the root of the diagram, one of the element types, a bookmarked + * node or a node/edge reference. + */ + 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",""); + } + } + + /** + * Locates on the haptic device the node or edge currently selected on the diagram tree. A command + * is sent to the haptic device which in turns drag the user to the node/edge location. + */ + 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)); + } + + /** + * Selects on the diagram tree the node or edge that's currently being touched by the haptic + * device. + */ + public void hHighlight() { + getActiveTab().getTree().jumpTo(hapticHighlightDiagramElement); + iLog("highlight " +((hapticHighlightDiagramElement instanceof Node)? "node" : "edge"),DiagramElement.toLogString(hapticHighlightDiagramElement)); + } + + /** + * Sends a command to the haptic device to pick up an element (node or edge) for + * moving it. + * + * @param de the diagram element to be picked up + */ + public void hPickUp(DiagramElement de) { + HapticsFactory.getInstance().pickUp(System.identityHashCode(de)); + } + + /** + * Prompts the user for an object insertion. The object can be a node, an edge, a property, + * a modifier, an edge label, an edge arrow head. Which object is to be inserted depends on + * the currently selected tree node on the tree. + */ + 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); + + /* remove the selections on the edge's nodes and release their lock */ + tree.clearNodeSelections(); + for(int i=0; i<edge.getNodesNum();i++){ + modelUpdater.yieldLock(edge, Lock.MUST_EXIST,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.INSERT_EDGE,edge.getId(),edge.getName())); + } + }else{ // adding a Node + iLog("insert node ",DiagramElement.toLogString(diagramElement)); + modelUpdater.insertInTree(diagramElement); + } + } catch (ConnectNodesException cne) { + final String message = cne.getLocalizedMessage(); + SoundFactory.getInstance().play(SoundEvent.ERROR, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(message); + } + }); + SoundFactory.getInstance().play(SoundEvent.ERROR); + iLog("insert edge error",message); + } + }else if(treeNode instanceof PropertyTypeMutableTreeNode){ //adding a property + PropertyTypeMutableTreeNode propTypeNode = (PropertyTypeMutableTreeNode)treeNode; + Node n = (Node)propTypeNode.getNode(); + if(!modelUpdater.getLock(n, Lock.PROPERTIES, new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.ADD_PROPERTY,n.getId(),n.getName()))){ + iLog("Could not get lock on node for add properties",DiagramElement.toLogString(n)); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.properties"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + + iLog("open insert property dialog",""); + final String propertyValue = SpeechOptionPane.showInputDialog(EditorFrame.this, + MessageFormat.format(resources.getString("dialog.input.property.text"), propTypeNode.getName()), + "" + ); + if(propertyValue != null){ + if(!propertyValue.isEmpty()){ //check that the user didn't enter an empty string + iLog("insert property ", propTypeNode.getType()+" "+propertyValue); + modelUpdater.addProperty(n, propTypeNode.getType(), propertyValue, DiagramEventSource.TREE); + }else{ + SoundFactory.getInstance().play(SoundEvent.EMPTY); + iLog("insert property", ""); + } + }else{ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel insert property dialog",""); + } + modelUpdater.yieldLock(n, Lock.PROPERTIES,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.ADD_PROPERTY,n.getId(),n.getName())); + }else if(treeNode instanceof PropertyMutableTreeNode){ // edit modifiers + iLog("open modifiers dialog",""); + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)treeNode.getParent(); + Node n = (Node)typeNode.getNode(); + Modifiers modifiers = n.getProperties().getModifiers(typeNode.getType()); + if(modifiers.isNull()){ + iLog("error:no modifiers for this property",""); + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("dialog.warning.null_modifiers"),typeNode.getType())); + }else{ + if(!modelUpdater.getLock(n, Lock.PROPERTIES,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_MODIFIERS,n.getId(),n.getName()))){ + iLog("Could not get lock on node for set modifiers",DiagramElement.toLogString(n)); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.properties"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + + int index = typeNode.getIndex(treeNode); + Set<Integer> result = SpeechOptionPane.showModifiersDialog(EditorFrame.this, + MessageFormat.format(resources.getString("dialog.input.check_modifiers"), + n.getProperties().getValues(typeNode.getType()).get(index)) , + modifiers.getTypes(), + modifiers.getIndexes(index) + ); + if(result == null){ + iLog("cancel modifiers dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + }else{ + iLog("edit modifiers",Arrays.toString(result.toArray())); + modelUpdater.setModifiers(n, typeNode.getType(), index, result,DiagramEventSource.TREE); + } + modelUpdater.yieldLock(n, Lock.PROPERTIES,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_MODIFIERS,n.getId(),n.getName())); + } + }else{ //NodeReferenceMutableTreeNode = edit label and arrow head + NodeReferenceMutableTreeNode nodeRef = (NodeReferenceMutableTreeNode)treeNode; + Node n = (Node)nodeRef.getNode(); + Edge e = (Edge)nodeRef.getEdge(); + if(!modelUpdater.getLock(e, Lock.EDGE_END,new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_ENDLABEL,e.getId(),e.getName()))){ + iLog("Could not get lock on edge for end label",DiagramElement.toLogString(e)); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.end_label"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + iLog("open edge operation selection dialog",""); + + boolean hasAvailArrowHeads = (e.getAvailableEndDescriptions().length > 0); + String[] operations = new String[hasAvailArrowHeads ? 2 : 1]; + operations[0] = resources.getString("dialog.input.edge_operation.label"); + if(hasAvailArrowHeads) + operations[1] = resources.getString("dialog.input.edge_operation.arrow_head"); + String choice = (String)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + resources.getString("dialog.input.edge_operation.select"), + operations, + operations[0]); + + if(choice == null){ + iLog("cancel edge operation selection dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + modelUpdater.yieldLock(e, Lock.EDGE_END, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_ENDLABEL,e.getId(),e.getName())); + return; + } + if(choice.equals(operations[0])){ //operations[0] = edit edge end-label + iLog("open edge label dialog",""); + String label = SpeechOptionPane.showInputDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.input.edge_label"),n.getType(), n.getName()), + e.getEndLabel(n) ); + if(label != null){ + modelUpdater.setEndLabel(e, n, label,DiagramEventSource.TREE); + SoundFactory.getInstance().play(SoundEvent.OK, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(tree.currentPathSpeech()); + } + }); + }else{ + iLog("cancel edge label dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + }else{//operations[1] = edit edge arrow head + String[] endDescriptions = new String[e.getAvailableEndDescriptions().length+1]; + for(int i=0;i<e.getAvailableEndDescriptions().length;i++) + endDescriptions[i] = e.getAvailableEndDescriptions()[i]; + endDescriptions[endDescriptions.length-1] = Edge.NO_ENDDESCRIPTION_STRING; + + iLog("open edge arrow head dialog",""); + final String endDescription = (String)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.input.edge_arrowhead"),n.getType(), n.getName()), + endDescriptions, + endDescriptions[0] + ); + if(endDescription != null){ + int index = Edge.NO_END_DESCRIPTION_INDEX; + for(int i=0;i<e.getAvailableEndDescriptions().length;i++) + if(endDescription.equals(e.getAvailableEndDescriptions()[i])){ + index = i; + break; + } + modelUpdater.setEndDescription(e, n, index,DiagramEventSource.TREE); + }else{ + iLog("cancel edge arrow head dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + } + modelUpdater.yieldLock(e, Lock.EDGE_END, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_ENDLABEL,e.getId(),e.getName())); + } + } + + /** + * Prompts the user for an object deletion. The object can be a node, an edge, a property, + * a modifier, an edge label, an edge arrow head. Which object is to be deleted depends on + * the currently selected tree node on the tree. + */ + public void delete(){ + DiagramPanel dPanel = getActiveTab(); + final DiagramTree tree = dPanel.getTree(); + final DiagramTreeNode treeNode = (DiagramTreeNode)tree.getSelectionPath().getLastPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + if(treeNode instanceof DiagramElement){ //delete a diagram element + final DiagramElement element = (DiagramElement)treeNode; + boolean isNode = element instanceof Node; + if(!modelUpdater.getLock(element, + Lock.DELETE, + new DiagramEventActionSource(DiagramEventSource.TREE, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + element.getId(),element.getName()))){ + iLog("Could not get lock on element for deletion",DiagramElement.toLogString(element)); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.delete"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + iLog("open delete "+ (isNode ? "node" : "edge") +" dialog",""); + int choice = SpeechOptionPane.showConfirmDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.confirm.deletion"),element.getType(), element.getName()), + SpeechOptionPane.OK_CANCEL_OPTION); + if(choice != SpeechOptionPane.OK_OPTION){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel delete " + (isNode ? "node" : "edge") +" dialog",""); + modelUpdater.yieldLock(element, + Lock.DELETE, + new DiagramEventActionSource(DiagramEventSource.TREE, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + element.getId(), + element.getName())); + return; + } + modelUpdater.takeOutFromTree(element); + /* don't need to unlock because the object doesn't exist any more, but * + * still need to make other users aware that the deletion process is finished */ + modelUpdater.sendAwarenessMessage( + AwarenessMessage.Name.STOP_A, + new DiagramEventActionSource(DiagramEventSource.TREE, + isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE, + element.getId(),element.getName()) + ); + }else if(treeNode.getParent() instanceof PropertyTypeMutableTreeNode){ //deleting a property + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)treeNode.getParent(); + Node n = (Node)typeNode.getNode(); + if(!modelUpdater.getLock(n, + Lock.PROPERTIES, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.REMOVE_PROPERTY,n.getId(),n.getName()))){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.properties"), SpeechOptionPane.INFORMATION_MESSAGE); + iLog("Could not get lock for properties for deletion",DiagramElement.toLogString(n)); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + iLog("open delete property dialog",""); + int choice = SpeechOptionPane.showConfirmDialog( + EditorFrame.this, + MessageFormat.format(resources.getString("dialog.confirm.deletion"),typeNode.getType(),treeNode.getName()), + SpeechOptionPane.OK_CANCEL_OPTION); + if(choice != SpeechOptionPane.OK_OPTION){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel delete property dialog",""); + modelUpdater.yieldLock(n, + Lock.PROPERTIES, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.REMOVE_PROPERTY,n.getId(),n.getName())); + return; + } + modelUpdater.removeProperty(n, typeNode.getType(), typeNode.getIndex(treeNode),DiagramEventSource.TREE); + }else + throw new IllegalStateException("Cannot delete a tree node of type: "+treeNode.getClass().getName()); + } + + /** + * Prompts the user for an object deletion. The object can be a node, an edge or a property. + * Which object is to be renamed depends on the currently selected tree node on the tree. + */ + public void rename(){ + DiagramPanel dPanel = getActiveTab(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + final DiagramTree tree = dPanel.getTree(); + DiagramTreeNode treeNode = (DiagramTreeNode)tree.getSelectionPath().getLastPathComponent(); + MessageFormat formatter = new MessageFormat(resources.getString("dialog.input.rename")); + if(treeNode instanceof DiagramElement){ + DiagramElement element = (DiagramElement)dPanel.getTree().getSelectionPath().getLastPathComponent(); + Object arg[] = {element.getName()}; + boolean isNode = element instanceof Node; + if(!modelUpdater.getLock(element, + Lock.NAME, + new DiagramEventActionSource(DiagramEventSource.TREE, + isNode ? Command.Name.SET_NODE_NAME : Command.Name.SET_EDGE_NAME,element.getId(),element.getName()))){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.name"),SpeechOptionPane.INFORMATION_MESSAGE); + iLog("Could not get lock on element for renaming",DiagramElement.toLogString(element)); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + iLog("open rename "+(isNode ? "node" : "edge")+" dialog",DiagramElement.toLogString(element)); + String name = SpeechOptionPane.showInputDialog(EditorFrame.this, + formatter.format(arg), + element.getName()); + if(name != null){ + modelUpdater.setName(element,name,DiagramEventSource.TREE); + }else{ + iLog("cancel rename "+(isNode ? "node" : "edge")+" dialog",DiagramElement.toLogString(element)); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + modelUpdater.yieldLock(element, + Lock.NAME, + new DiagramEventActionSource( + DiagramEventSource.TREE, + isNode ? Command.Name.SET_NODE_NAME : Command.Name.SET_EDGE_NAME, + element.getId(), + element.getName())); + }else if(treeNode instanceof PropertyMutableTreeNode){ + PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)treeNode.getParent(); + Node n = (Node)typeNode.getNode(); + if(!modelUpdater.getLock(n, + Lock.PROPERTIES, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_PROPERTY,n.getId(),n.getName()))){ + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.properties"), SpeechOptionPane.INFORMATION_MESSAGE); + iLog("Could not get lock on properties for renaming",DiagramElement.toLogString(n)); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + Object arg[] = {treeNode.getName()}; + iLog("open rename property dialog",treeNode.getName()); + String name = SpeechOptionPane.showInputDialog(EditorFrame.this, + formatter.format(arg), + treeNode.getName()); + if(name == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel rename property dialog",treeNode.getName()); + return; + } + modelUpdater.setProperty(n, typeNode.getType(), typeNode.getIndex(treeNode), name,DiagramEventSource.TREE); + modelUpdater.yieldLock(n, + Lock.PROPERTIES, + new DiagramEventActionSource(DiagramEventSource.TREE,Command.Name.SET_PROPERTY,n.getId(),n.getName())); + }else + throw new IllegalStateException("Cannot delete a tree node of type: "+treeNode.getClass().getName()); + } + + /** + * Prompts the user with a dialog to add or remove bookmarks on a tree node. Which node is to be + * (un)bookmarked depends on the currently selected tree node on the tree. + */ + public void editBookmarks(){ + boolean addBookmark = true; + DiagramPanel dPanel = getActiveTab(); + final DiagramTree tree = dPanel.getTree(); + DiagramTreeNode treeNode = (DiagramTreeNode)tree.getLastSelectedPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + + if(!modelUpdater.getLock(treeNode, + Lock.BOOKMARK, + DiagramEventActionSource.NULL)){ + iLog("Cannot get lock on tree node for bookmark", treeNode.getName()); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.bookmark"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + + if(!treeNode.getBookmarkKeys().isEmpty()){ + /* the are already bookmarks, thus we let the user chose whether they want to */ + /* add a new one or remove an old one */ + String[] options = { + resources.getString("dialog.input.bookmark.select.add"), + resources.getString("dialog.input.bookmark.select.remove") + }; + + iLog("open select add/remove bookmark dialog",""); + String result = (String)SpeechOptionPane.showSelectionDialog(EditorFrame.this, + resources.getString("dialog.input.bookmark.select.add_remove"), + options, + options[0]); + + if(result == null){ + iLog("cancel select add/remove bookmark dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + modelUpdater.yieldLock(treeNode, Lock.BOOKMARK, DiagramEventActionSource.NULL); + return; + } + if(result.equals(options[1])) + addBookmark = false; + } + + if(addBookmark){ + boolean uniqueBookmarkChosen = false; + while(!uniqueBookmarkChosen){ + iLog("open add bookmark dialog",""); + String bookmark = SpeechOptionPane.showInputDialog(EditorFrame.this, resources.getString("dialog.input.bookmark.text"),""); + if(bookmark != null){ + if("".equals(bookmark)){ + iLog("error: entered empty bookmark",""); + SoundFactory.getInstance().play(SoundEvent.ERROR);// without listeners in order not to overwrite the speechdialog popping up again + NarratorFactory.getInstance().speakWholeText(resources.getString("dialog.input.bookmark.text.empty")); + }else if(tree.getModel().getBookmarks().contains(bookmark)){ + iLog("error: entered bookmark already existing",bookmark); + SoundFactory.getInstance().play(SoundEvent.ERROR); + NarratorFactory.getInstance().speakWholeText(resources.getString("dialog.input.bookmark.text.already_existing")); + }else{ + tree.getModel().putBookmark(bookmark, treeNode,DiagramEventSource.TREE); + uniqueBookmarkChosen = true; + } + }else{ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel add bookmark dialog",""); + break; //user no longer wants to choose, exit the dialog thus + } + } + }else{ // removing a bookmark + String[] bookmarksArray = new String[treeNode.getBookmarkKeys().size()]; + bookmarksArray = treeNode.getBookmarkKeys().toArray(bookmarksArray); + + iLog("open remove bookmark dialog",""); + final String bookmark = (String)SpeechOptionPane.showSelectionDialog( + EditorFrame.this, + resources.getString("dialog.input.bookmark.delete"), + bookmarksArray, + bookmarksArray[0] + ); + + if(bookmark != null){ + tree.getModel().removeBookmark(bookmark,DiagramEventSource.TREE); + }else{ + iLog("cancel remove bookmark dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + } + modelUpdater.yieldLock(treeNode, Lock.BOOKMARK, DiagramEventActionSource.NULL); + } + + /** + * Prompts the user with a dialog edit notes on a tree node. Which node is to be + * noted depends on the currently selected tree node on the tree. + */ + public void editNotes(){ + DiagramPanel dPanel = getActiveTab(); + DiagramTreeNode treeNode = (DiagramTreeNode)dPanel.getTree().getLastSelectedPathComponent(); + DiagramModelUpdater modelUpdater = dPanel.getDiagram().getModelUpdater(); + if(!modelUpdater.getLock(treeNode, Lock.NOTES, DiagramEventActionSource.NULL)){ + iLog("Could not get lock on tree node for notes",treeNode.getName()); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.lock_failure.notes"), SpeechOptionPane.INFORMATION_MESSAGE); + SoundFactory.getInstance().play(SoundEvent.MESSAGE_OK); + return; + } + + String typeString = ""; + /* if the note is for a diagram element the dialog message is changed so that the type precedes the name */ + if(treeNode instanceof DiagramElement){ + typeString = ((DiagramElement)treeNode).getType() + " "; + } + /* if the note is for a property tree node the dialog message is changed so that the type precedes the name */ + if(treeNode instanceof PropertyMutableTreeNode){ + PropertyTypeMutableTreeNode parent = (PropertyTypeMutableTreeNode)treeNode.getParent(); + typeString = parent.getType() + " "; + } + iLog("open edit note dialog",""); + String result = SpeechOptionPane.showTextAreaDialog(EditorFrame.this, resources.getString("dialog.input.notes.text")+typeString+treeNode.getName() ,treeNode.getNotes()); + if(result != null){ + modelUpdater.setNotes(treeNode, result,DiagramEventSource.TREE); + }else{ + iLog("cancel edit note dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + } + modelUpdater.yieldLock(treeNode, Lock.NOTES,DiagramEventActionSource.NULL); + } + + /** + * Starts the server on a background thread. + */ + public void startServer(){ + iLog("server started",""); + /* If the awareness filter has not been created yet (by opening the awareness filter dialog) then create it */ + BroadcastFilter filter = BroadcastFilter.getInstance(); + if(filter == null) + try{ + filter = BroadcastFilter.createInstance(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(this, ioe.getLocalizedMessage()); + } + /* create the server */ + server = Server.createServer(); + try{ + server.init(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + ioe.getLocalizedMessage()); + iLog("error: starting server",ioe.getLocalizedMessage()); + return; + } + server.start(); + startServerMenuItem.setEnabled(false); + stopServerMenuItem.setEnabled(true); + if(getActiveTab() != null && (!(getActiveTab().getDiagram() instanceof NetDiagram))) + shareDiagramMenuItem.setEnabled(true); + } + + /** + * Stops the running server + */ + public void stopServer(){ + /* those network diagrams which are connected to the local server are reverted, * + * that is the diagram panel is set with the delegate diagram of the network diagram */ + for(int i=0; i < editorTabbedPane.getTabCount(); i++){ + DiagramPanel dPanel = editorTabbedPane.getComponentAt(i); + if(dPanel.getDiagram() instanceof NetDiagram){ + NetDiagram netDiagram = (NetDiagram)dPanel.getDiagram(); + if(netDiagram.getSocketChannel().equals(localSocket)){ + dPanel.setAwarenessPanelEnabled(false); + dPanel.setDiagram(netDiagram.getDelegate()); + } + } + } + server.shutdown(resources.getString("server.shutdown_msg")); + server = null; + if(localSocket != null){ + try{localSocket.close();}catch(IOException ioe){ioe.printStackTrace();} + localSocket = null; + } + startServerMenuItem.setEnabled(true); + stopServerMenuItem.setEnabled(false); + shareDiagramMenuItem.setEnabled(false); + if(getActiveTab() != null) + fileCloseItem.setEnabled(true); + iLog("server stopped",""); + } + + /** + * Makes a diagram shared on the server. When a diagram is shared, remote users can connect to the + * server and edit it collaboratively with the local and the other connected users. + */ + public void shareDiagram(){ + try{ + if(server == null) + throw new DiagramShareException(resources.getString("server.not_running_exc")); + + DiagramPanel dPanel = getActiveTab(); + Diagram diagram = dPanel.getDiagram(); + try { + iLog("share diagram",diagram.getName()); + /* check if it's already connected to the local server (a.k.a. another diagram has been shared previously */ + if(localSocket == null){ + int port = Integer.parseInt(PreferencesService.getInstance().get("server.local_port",Server.DEFAULT_REMOTE_PORT)); + InetSocketAddress address = new InetSocketAddress("127.0.0.1",port); + localSocket = SocketChannel.open(address); + } + + server.share(diagram); + ProtocolFactory.newInstance().send(localSocket, new Command(Command.Name.LOCAL,diagram.getName(),DiagramEventSource.NONE)); + dPanel.setDiagram(NetDiagram.wrapLocalHost(diagram,localSocket,server.getLocalhostQueue(diagram.getName()),netLocalDiagramExceptionHandler)); + /* share a diagram once is enought :) */ + shareDiagramMenuItem.setEnabled(false); + /* no close, there might be clients connected. unshare first. */ + fileCloseItem.setEnabled(false); + /* only enabled for local diagram, otherwise changing the name * + * of the diagram would messes up the network protocol */ + fileSaveAsItem.setEnabled(false); + dPanel.setAwarenessPanelEnabled(true); + } catch (IOException e) { + iLog("error sharing diagram",diagram.getName()+" "+e.getLocalizedMessage()); + SpeechOptionPane.showMessageDialog(EditorFrame.this, e.getLocalizedMessage()); + return; + } + }catch(DiagramShareException dse){ + SpeechOptionPane.showMessageDialog(EditorFrame.this, dse.getLocalizedMessage()); + } + } + + /** + * Prompts the user for a server address and connect to the server. The server is queried for the list + * of the shared diagrams and the user is prompted with a selection dialog to chose a diagram to download. + * The diagram is then downloaded and loaded into the diagram editor, ready for shared editing. + * + */ + public void openSharedDiagram(){ + iLog("open open share diagram dialog",""); + /* open the window prompting for the server address and make checks on the user input */ + String addr = SpeechOptionPane.showInputDialog( + EditorFrame.this, + resources.getString("dialog.share_diagram.enter_address"), + PreferencesService.getInstance().get("server.address", "")); + if(addr == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + iLog("cancel open share diagram dialog",""); + return; + }else if(!ProtocolFactory.validateIPAddr(addr)){ + iLog("error:invalid IP address",addr); + SpeechOptionPane.showMessageDialog(this, resources.getString("dialog.share_diagram.wrong_ip")); + return; + }else{ + PreferencesService.getInstance().put("server.address", addr); + /* open the channel for the new diagram */ + SocketChannel channel = null; + try { + channel = SocketChannel.open(); + } catch (IOException e) { + iLog("error:could not connect to the server",""); + SpeechOptionPane.showMessageDialog(EditorFrame.this, resources.getString("dialog.error.no_connection_to_server")); + return; + } + /* download the diagram list */ + DiagramDownloader downloader = new DiagramDownloader( + channel, + addr, + DiagramDownloader.CONNECT_AND_DOWNLOAD_LIST_TASK + ); + iLog("open download diagram list dialog",""); + int option = SpeechOptionPane.showProgressDialog(EditorFrame.this, resources.getString("dialog.downloading_diagram_list"), downloader,500); + if(option == SpeechOptionPane.CANCEL_OPTION){ + iLog("cancel download diagram list dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();} + }else{ + try{ + /* show the available diagram list */ + String result = downloader.get(); + if(result == null) + throw new Exception(resources.getString("dialog.error.no_diagrams_on_server")); // go to the catch block + String[] diagramsList = result.split("\n"); + + iLog("open select diagram to download dialog",""); + String diagramName = (String)SpeechOptionPane.showSelectionDialog(EditorFrame.this, "Select diagram to download", diagramsList, diagramsList[0]); + if(diagramName == null){ + iLog("cancel select diagram to download dialog",""); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();} + return; + } + /* there cannot be two diagrams with the same name open at the same time */ + if(editorTabbedPane.getDiagramNameTabIndex(diagramName) != -1) + throw new IOException(resources.getString("dialog.error.same_file_name")); + /* download the chosen diagram */ + downloader = new DiagramDownloader(channel,diagramName,DiagramDownloader.DOWNLOAD_DIAGRAM_TASK); + iLog("open downloading diagram dialog",diagramName); + option = SpeechOptionPane.showProgressDialog(EditorFrame.this, MessageFormat.format(resources.getString("dialog.downloading_diagram"), diagramName), downloader,500); + if(option == SpeechOptionPane.CANCEL_OPTION){ + iLog("cancel downloading diagram dialog",diagramName); + SoundFactory.getInstance().play(SoundEvent.CANCEL); + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}; + }else{ + result = downloader.get(); + + if(clientConnectionManager == null || !clientConnectionManager.isAlive()){ + clientConnectionManager = new ClientConnectionManager(editorTabbedPane); + clientConnectionManager.start(); + } + + iLog("START READ NETWORK DIAGRAM "+diagramName); + // FIXME no pd patches supported + Diagram diagram = PersistenceManager.decodeDiagramInstance(new BufferedInputStream(new ByteArrayInputStream(result.getBytes("UTF-8")))); + iLog("END READ NETWORK DIAGRAM "+diagramName); + /* remove all the bookmarks in the server diagram model instance */ + for(String bookmarkKey : diagram.getTreeModel().getBookmarks()) + diagram.getTreeModel().removeBookmark(bookmarkKey,DiagramEventSource.TREE); + Diagram newDiagram = NetDiagram.wrapRemoteHost(diagram,clientConnectionManager,channel); + DiagramPanel dPanel = addTab(null,newDiagram); + /* enable awareness on the new diagram */ + dPanel.setAwarenessPanelEnabled(true); + /* make the network thread aware of the new shared diagram, from here on the messages received from the server will take effect */ + clientConnectionManager.addRequest(new ClientConnectionManager.AddDiagramRequest(channel, diagram)); + clientConnectionManager.addRequest(new SendAwarenessRequest(channel, new AwarenessMessage( + AwarenessMessage.Name.USERNAME_A, + newDiagram.getName(), + AwarenessMessage.getDefaultUserName() + ))); + } + }catch(RuntimeException rte){ + throw new RuntimeException(rte); + }catch(ExecutionException ee){ + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}; + /* if the exception happened in the DiagramDownloader then it's wrapped into an * + * ExecutionException and we have to unwrap it to get a neat message for the user */ + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + ee.getCause().getLocalizedMessage()); + iLog("error: "+ee.getCause().getMessage(),""); + }catch(Exception exception){ + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}; + SpeechOptionPane.showMessageDialog( + editorTabbedPane, + exception.getLocalizedMessage()); + iLog("error: "+exception.getMessage(),""); + } + } + } + } + + /** + * Shows the awareness panel, a text pane where all the awareness informations received by the server + * are displayed. + */ + public void showAwarenessPanel(){ + DiagramPanel dPanel = getActiveTab(); + if(dPanel != null){ + dPanel.setAwarenessPanelVisible(true); + NarratorFactory.getInstance().speak(resources.getString("speech.awareness_panel.open")); + } + } + + /** + * Hides the awareness panel, a text pane where all the awareness informations received by the server + * are displayed. + */ + public void hideAwarenessPanel(){ + DiagramPanel dPanel = getActiveTab(); + if(dPanel != null){ + dPanel.setAwarenessPanelVisible(false); + NarratorFactory.getInstance().speak(resources.getString("speech.awareness_panel.close")); + dPanel.getTree().requestFocus(); + } + } + + /** + * Saves all the open diagram which have been modified since the last time they were saved into the + * <i>backup</i> folder in the ccmi_editor_data directory. + */ + public void backupOpenDiagrams(){ + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE_d_MMM_yyyy_HH_mm_ss"); + String date = dateFormat.format(new Date()); + File backupDir = new File(new StringBuilder(backupDirPath) + .append(System.getProperty("file.separator")) + .append(date) + .toString()); + backupDir.mkdir(); + for(int i=0; i<editorTabbedPane.getTabCount();i++){ + DiagramPanel dPanel = editorTabbedPane.getComponentAt(i); + if(dPanel.isModified()||dPanel.getFilePath() == null){ + Diagram diagram = dPanel.getDiagram(); + File file = new File(backupDir,diagram.getName()+".ccmi"); + try { + FileService.Save save = new FileService.DirectService().save((file)); + if(diagram instanceof PdDiagram){ + PdPersistenceManager.getInstance().encodeDiagramInstance(diagram, save.getOutputStream()); + }else { + PersistenceManager.encodeDiagramInstance(diagram, save.getOutputStream()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + Exports the current graph to an image file. + */ + public void exportImage(){ + DiagramPanel dPanel = getActiveTab(); + if (dPanel == null) + return; + OutputStream out = null; + try{ + String imageExtensions = resources.getString("files.image.extension"); + /* default save dir is the same as the diagram's or home/images otherwise */ + String path = dPanel.getFilePath(); + if(path == null) + path = PreferencesService.getInstance().get("dir.images", "."); + FileService.Save save = fileService.save(path, dPanel.getDiagram().getName(), exportFilter, + defaultExtension, imageExtensions,null); + out = save.getOutputStream(); + if (out != null){ + /* if the diagram has a name (has already been saved) then prompt the user with the name of + * the diagram with a jpg extension. */ + String fileName = FileService.getFileNameFromPath(save.getPath(),true); + String extension = fileName.substring(fileName.lastIndexOf(".") + 1); + if (!ImageIO.getImageWritersByFormatName(extension).hasNext()){ + throw new IOException(MessageFormat.format( + resources.getString("dialog.error.unsupported_image"), + extension + )); + } + GraphPanel gPanel = dPanel.getGraphPanel(); + try{ + saveImage(gPanel, out, extension); + speakFocusedComponent(resources.getString("dialog.file_saved")); + }catch(IOException ioe){ + throw new IOException(resources.getString("dialog.error.save_image"),ioe); + } + } + } + catch (IOException ioe){ + SpeechOptionPane.showMessageDialog(editorTabbedPane,ioe.getMessage()); + }finally{ + if(out != null) + try{out.close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + + /** + Exports a current graph to an image file. + @param graph the graph + @param out the output stream + @param format the image file format + + @throws IOException if something goes wrong during the I/O operations + */ + public static void saveImage(GraphPanel graph, OutputStream out, String format) + throws IOException { + // need a dummy image to get a Graphics to measure the size + Rectangle2D bounds = graph.getBounds(); + BufferedImage image + = new BufferedImage((int)bounds.getWidth() + 1, + (int)bounds.getHeight() + 1, + BufferedImage.TYPE_INT_RGB); + Graphics2D g2 = (Graphics2D)image.getGraphics(); + g2.translate(-bounds.getX(), -bounds.getY()); + g2.setColor(Color.WHITE); + g2.fill(new Rectangle2D.Double( + bounds.getX(), + bounds.getY(), + bounds.getWidth() + 1, + bounds.getHeight() + 1)); + g2.setColor(Color.BLACK); + g2.setBackground(Color.WHITE); + boolean hideGrid = graph.getHideGrid(); + graph.setHideGrid(true); + graph.paintComponent(g2); + graph.setHideGrid(hideGrid); + ImageIO.write(image, format, out); + } + + /** + * Shows the configuration dialog of the broadcast filter. The broadcast filter affects + * which awareness informations are broadcasted from the server to the other clients. + * If the local editor is not running the server, changes to the broadcast filter + * will have no effect. + */ + public void showAwarenessBroadcastDialog(){ + BroadcastFilter filter = BroadcastFilter.getInstance(); + if(filter == null) + try{ + filter = BroadcastFilter.createInstance(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(this, ioe.getLocalizedMessage()); + } + filter.showDialog(this); + } + + /** + * Shows the configuration dialog of the display filter. The display filter affects + * which awareness informations are received from the server are actually displayed + * to the user. + */ + public void showAwarenessDisplayDialog(){ + DisplayFilter filter = DisplayFilter.getInstance(); + if(filter == null) + try{ + filter = DisplayFilter.createInstance(); + }catch(IOException ioe){ + SpeechOptionPane.showMessageDialog(this, ioe.getLocalizedMessage()); + } + filter.showDialog(this); + } + + /** + * Prompts the user with a dialog to choose he awareness username. The username is used in the + * awareness information to identify which client is doing the actions that are being notified. + */ + public void showAwarnessUsernameDialog(){ + String oldName = AwarenessMessage.getDefaultUserName(); + String newName = SpeechOptionPane.showInputDialog( + this, + resources.getString("dialog.input.awerness_username"), + oldName + ); + if(newName == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return; + } + + if(newName.trim().isEmpty()){ + SoundFactory.getInstance().play(SoundEvent.ERROR, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(resources.getString("speech.empty_userame")); + } + }); + return; + } + + if(!newName.equals(oldName)){//if the name hasn't changed don't issue any message + PreferencesService.getInstance().put("user.name", newName); + AwarenessMessage.setDefaultUserName(newName); + NarratorFactory.getInstance().speak(MessageFormat.format( + resources.getString("dialog.feedback.awareness_username"), + newName + )); + for(int i=0; i<editorTabbedPane.getTabCount(); i++){ + Diagram diagram = editorTabbedPane.getComponentAt(i).getDiagram(); + diagram.getModelUpdater().sendAwarenessMessage( + AwarenessMessage.Name.USERNAME_A, + newName);// send the new name only, the old name will be added by the server + } + } + } + + /** + * Prompts the user with a dialog to choose the port number the local server will listen on. + */ + public void showLocalServerPortDialog(){ + showServerPortDialog("server.local_port","dialog.input.local_server_port","dialog.feedback.local_server_port"); + } + + /** + * Prompts the user with a dialog to choose the remote server port number to connect to. + */ + public void showRemoteServerPortDialog(){ + showServerPortDialog("server.remote_port","dialog.input.remote_server_port","dialog.feedback.remote_server_port"); + } + + private void showServerPortDialog(String preferenceKey,String dialogMessage,String feedbackMessage){ + String oldPort = PreferencesService.getInstance().get(preferenceKey,Server.DEFAULT_LOCAL_PORT); + String newPort = SpeechOptionPane.showInputDialog(this, resources.getString(dialogMessage), oldPort); + if(newPort == null){ + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return; + } + + boolean badFormat = false; + try { + int port = Integer.parseInt(newPort); + if(port <= 0 || port > 65535) + badFormat = true; + }catch(NumberFormatException nfe){ + badFormat = true; + } + + if(badFormat){ + SoundFactory.getInstance().play(SoundEvent.ERROR, new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(resources.getString("speech.bad_format_port")); + } + }); + return; + } + PreferencesService.getInstance().put(preferenceKey,newPort); + NarratorFactory.getInstance().speak(MessageFormat.format( + resources.getString(feedbackMessage),newPort)); + } + + /** + Displays the About dialog box. + */ + public void showAboutDialog(){ + String options[] = {resources.getString("dialog.ok_button")}; + SpeechSummaryPane.showDialog(this, + resources.getString("dialog.about.title"), + MessageFormat.format(resources.getString("dialog.about"), + resources.getString("app.name"), + resources.getString("app.version"), + resources.getString("dialog.about.description"), + resources.getString("dialog.about.license")), + SpeechSummaryPane.OK_OPTION, + options + ); + } + + /** + * Displays the Software license in a dialog box. + */ + 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();} + } + } + + /** + * Saves the diagram template in the <i>templates</i> folder. + * + * The template can then be reused to create new diagrams with the same type of nodes and + * edges. + * + * @param diagram the diagram to get the template from + * @throws IOException if something goes wrong with I/O when saving the file + */ + 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 diagram type to the File->New menu. + * + * @param diagram the diagram whose nodes and edges definition will be used as a template + * for new diagrams creation. + * + */ + 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); + JMenuItem newTypeItem = SpeechMenuFactory.getMenuItem(diagram.getName()); + newTypeItem.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent event){ + Diagram clone = (Diagram)diagram.clone(); + /* find a good unique name for the new tab */ + Pattern pattern = Pattern.compile("new "+clone.getName()+"( \\(([0-9]+)\\))?"); + int maxOpenDiagram = -1; + for(int i=0;i<editorTabbedPane.getTabCount();i++){ + Matcher matcher = pattern.matcher(editorTabbedPane.getComponentAt(i).getDiagram().getName()); + if(matcher.matches()){ + if(matcher.group(1) == null) + maxOpenDiagram = 0; + else + maxOpenDiagram = Math.max(maxOpenDiagram, Integer.parseInt(matcher.group(2))); + } + } + if(maxOpenDiagram >= 0) + clone.setName(String.format("new %s (%d)", clone.getName(),++maxOpenDiagram)); + else clone.setName("new "+clone.getName()); + addTab(null, clone); + iLog("new diagram created of type: "+diagram.getName()); + } + }); + newMenu.add(newTypeItem); + } + + /** + * 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); + } + + /** + * Returns the currently selected tab's diagram panel. + * + * @return the currently selected tab's diagram panel or {@code null} + * if no tab is open. + */ + public DiagramPanel getActiveTab(){ + return (DiagramPanel)editorTabbedPane.getSelectedComponent(); + } + + /** + * Set the variable holding the node or edge that would be highlighted if + * {@link #hHighlight()} is called. The menu item for highlight is also enabled + * if {@code de} is not {@code null}. + * + * @param de the diagram element to be selected by {@code hHighlight()} + * or {@code null} for no selection. + */ + 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()); + 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(); + haptics.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(); + return diagramPanel; + } + + private void diagramPanelEnabledMenuUpdate(DiagramPanel dPanel){ + fileSaveItem.setEnabled(false); + fileSaveAsItem.setEnabled(false); + fileSaveCopyItem.setEnabled(false); + fileCloseItem.setEnabled(false); + shareDiagramMenuItem.setEnabled(false); + graphExportItem.setEnabled(false); + showAwarenessPanelMenuItem.setEnabled(false); + hideAwarenessPanelMenuItem.setEnabled(false); + if(dPanel == null) + return; + + fileSaveItem.setEnabled(true); + fileSaveCopyItem.setEnabled(true); + graphExportItem.setEnabled(true); + if(dPanel.getDiagram() instanceof NetDiagram){ + if(dPanel.isAwarenessPanelVisible()) + hideAwarenessPanelMenuItem.setEnabled(true); + else + showAwarenessPanelMenuItem.setEnabled(true); + }else{ + /* only enabled for local diagram, otherwise changing the name * + * of the diagram would messes up the network protocol */ + fileSaveAsItem.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){ + + /* add the pd diagam type first */ + addDiagramType(new PdDiagram()); + + boolean someFilesNotRead = false; + for(File file : files){ + try { + Diagram d = PersistenceManager.decodeDiagramTemplate(file); + addDiagramType(d); + } catch (IOException e) { + someFilesNotRead = true; + e.printStackTrace(); + } + } + return someFilesNotRead; + } + + private void iLog(String action,String args){ + InteractionLog.log("TREE",action,args); + } + + private void iLog(String message){ + InteractionLog.log(message); + } + + private Server server; + private SocketChannel localSocket; + private ExceptionHandler netLocalDiagramExceptionHandler; + private ClientConnectionManager clientConnectionManager; + private Haptics haptics; + private ResourceBundle resources; + public EditorTabbedPane editorTabbedPane; + private FileService.ChooserService fileService; + private PreferencesService preferences; + private HapticTrigger hapticTrigger; + private DiagramElement hapticHighlightDiagramElement; + private ArrayList<String> existingTemplateNames; + private ArrayList<Diagram> existingTemplates; + private AwarenessPanelEnablingListener awarenessPanelListener; + + private JMenu newMenu; + private JMenuItem jumpMenuItem; + private boolean canJumpRef; + private JMenuItem fileSaveItem; + private JMenuItem graphExportItem; + private JMenuItem fileSaveAsItem; + private JMenuItem fileSaveCopyItem; + private JMenuItem fileCloseItem; + private JMenuItem insertMenuItem; + private JMenuItem deleteMenuItem; + private JMenuItem renameMenuItem; + private JMenuItem selectMenuItem; + private JMenuItem bookmarkMenuItem; + private JMenuItem editNotesMenuItem; + private JMenuItem locateMenuItem; + private JMenuItem highlightMenuItem; + private JMenuItem shareDiagramMenuItem; + private JMenuItem startServerMenuItem; + private JMenuItem stopServerMenuItem; + private JMenuItem showAwarenessPanelMenuItem; + private JMenuItem hideAwarenessPanelMenuItem; + private TreeSelectionListener treeSelectionListener; + private ChangeListener tabChangeListener; + private String defaultExtension; + private String backupDirPath; + private ArrayList<String> recentFiles; + private JMenu recentFilesMenu; + private int maxRecentFiles = DEFAULT_MAX_RECENT_FILES; + + private ExtensionFilter extensionFilter; + private ExtensionFilter exportFilter; + + private static final int DEFAULT_MAX_RECENT_FILES = 5; + private static final double GROW_SCALE_FACTOR = Math.sqrt(2); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorFrame.properties Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,316 @@ +### APPLICATION ### +app.name=CCmI Editor +app.version=0.1.4 +files.name=CCmI Diagram files +files.extension=.ccmi +template.extension=.xml +dir.home=ccmi_editor +dir.templates=templates + +### EDITOR ### + +files.image.name=Image Files +files.image.extension=.jpg +grabber.text=Select + +window.focus=Editor window focused +window.unfocus=Editor window unfocused +window.tab=Tab, {0} +window.tree=Tree +window.no_tabs=No diagrams open + +#### DIALOGS ### +dialog.about={0} version {1}\u000A\u000A{2}\u000A\u000A{3} +dialog.about.description=Collaborative Cross-modal Interfaces\u000A \ +The Collaborative Cross-modal Interfaces (CCmI) project is a Research\u000A \ +Councils UK Digital Economy Programme funded project that aims to explore\u000A \ +the use of multi-modal input and output technologies (audio, haptics, graphics)\u000A \ +to improve the accessibility of collaboration using diagrams in the workplace.\u000A \ +The challenge is to design support for collaboration where participants have\u000A \ +differing access to modalities - we refer to these situations\u000A as cross-modal collaboration. +dialog.about.license=This program comes with ABSOLUTELY NO WARRANTY.\u000AThis is free software, and you are welcome to redistribute it\u000Aunder certain conditions.\ +Select License in the Help menu for details. +dialog.about.title=About +dialog.license.title=License + +dialog.ok_button=Ok +dialog.cancel_button=Cancel + +dialog.overwrite=Another file with the same name already exists. Do you want to overwrite it ? +dialog.properties=Properties +dialog.error.title=Error +dialog.error.filesnotread=Error: One or more templates files could not be read properly +dialog.error.local_server=Error: Problems in communication with local server +dialog.error.save_template=Error: could not save template +dialog.error.no_template_to_edit=Error: there are no template to edit +dialog.error.same_file_name=Cannot 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.error.log_enable=The log could not be enabled + +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} + +dialog.input.bookmark.select.add_remove=What would you like to do ? +dialog.input.bookmark.select.add=Add Bookmark +dialog.input.bookmark.select.remove=Remove Bookmark +dialog.input.bookmark.text=New bookmark. Enter name +dialog.input.bookmark.text.already_existing=The chosen bookmark already exists +dialog.input.bookmark.text.empty=Bookmarks cannot be empty +dialog.input.bookmark.select.bookmark=Select bookmark +dialog.input.bookmark.select.notfound=No bookmarks available +dialog.input.bookmark.delete=Select bookmark to remove +dialog.input.bookmark.title=Bookmark +dialog.input.notes.title=Notes +dialog.input.notes.text=Edit Notes for +dialog.input.property.text=New {0}, enter name +dialog.input.rename=Renaming {0}, Enter new name. +dialog.input.jump.select=Where would you like to jump to ? +dialog.input.edge_operation.label=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 +dialog.input.selected_type.select=Select which type to jump to +dialog.input.check_modifiers=Editing {0}. +dialog.input.edge_arrowhead=Select arrow head for {0} {1} +dialog.input.sound_rate=Select rate value +dialog.input.edit_diagram_template=Select Diagram to edit +dialog.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.input.haptics.select=Select the haptic device you want to use + +dialog.confirm.deletion=Are you sure you want to delete the {0} {1} ? +dialog.confirm.deletions=Are you sure you want to delete the selected objects ? +dialog.confirm.exit={0} Unsaved Diagrams\u000ADo you want to save changes? +dialog.confirm.close=Unsaved diagram.\u000ADo you want to save changes? +dialog.confirm.title=Confirm + +dialog.lock_failure.delete=Object is being edited by another user +dialog.lock_failure.deletes=Objects are being edited by other users +dialog.lock_failure.deletes_warning=The following objects will not be deleted as they're locked by other users: +dialog.lock_failure.name=Object name is being edited by another user +dialog.lock_failure.properties=Node properties are being edited by another user +dialog.lock_failure.end_label=Edge is being edited by another user +dialog.lock_failure.arrow_head=Edge arrow heads are being edited by another user +dialog.lock_failure.move=Object is being moved by another user +dialog.lock_failure.notes=Tree node is being edited by another user +dialog.lock_failure.bookmark=Tree node is being edited by another user +dialog.lock_failure.must_exist=Element is candidate for deletion by another user +dialog.lock_failure.no_edge_creation="One or more nodes are candidates for creation by another user" + +dialog.property_editor.title=Property Editor +dialog.property_editor.error.property_null=Properties cannot be null +dialog.property_editor.edit_modifiers_button=Edit Modifiers + +dialog.modifier_editor.title=Modifier Editor + +dialog.speech_option_pane.download=Download +dialog.speech_option_pane.input=Input +dialog.speech_option_pane.select=Select +dialog.speech_option_pane.confirm=Confirm +dialog.speech_option_pane.modifiers=Select Modifiers +dialog.speech_option_pane.message= {0}. Press OK to confirm +dialog.speech_option_pane.cancel=Cancel + +dialog.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.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} +dialog.feedback.haptic_init={0} selected\u000AYou need to restart the editor in order apply this change + +server.shutdown_msg=request by user +server.not_running_exc=Server not running +#### OPTIONS #### +options.jump.reference=Reference +options.jump.type=Type +options.jump.diagram=Diagram +options.jump.bookmark=Bookmark +#### SPEECH #### +speech.cancelled=Cancelled, {0} +speech.delete.element.ack={0} deleted, {1} +speech.deleted.property.ack={0} deleted, {1} +speech.delete.bookmark.ack= bookmark {0} deleted, {1} +speech.no_bookmarks=No bookmarks available +speech.note.updated=Note updated +speech.empty_property=Empty string, nothing created. {0} +speech.empty_label=Empty label +speech.empty_userame=User Name cannot be empty +speech.selected=selected +speech.unselected=unselected +speech.input.property.ack={0} created, +speech.input.node.ack={0} , created, +speech.input.edge.ack=, connected, +speech.input.edge.ack2= and +speech.invalid_ip=invalid IP address +speech.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 + +speech.haptic_window_open=Haptic Window open +speech.haptic_window_close=Haptic Window closed + +### 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 +file.new.mnemonic=N +file.open.text=Open... +file.open.mnemonic=O +file.open.accelerator=ctrl O +file.recent.text=Recent files +file.recent.mnemonic=R +file.save.text=Save +file.save.mnemonic=S +file.save.accelerator=ctrl S +file.save_copy.text=Save a copy... +file.save_as.text=Save as... +#file.save_as.mnemonic=A +file.close.text=Close +file.export_image.text=Export image +file.export_image.mnemonic=E +file.print.text=Print +file.print.mnemonic=P +file.exit.text=Exit +file.exit.mnemonic=X +file.exit.accelerator=ctrl X +edit.text=Edit +edit.mnemonic=E +edit.jump.text=Jump to +edit.jump.mnemonic=J +edit.jump.accelerator=ctrl J +edit.properties.text=Properties +edit.properties.mnemonic=P +edit.delete.text=Delete +edit.delete.mnemonic=D +edit.delete.accelerator=ctrl DELETE +edit.insert.text=Insert/Edit +edit.insert.mnemonic=i +edit.insert.accelerator=ctrl ENTER +edit.rename.text=Rename +edit.rename.mnemonic=R +edit.rename.accelerator=ctrl R +edit.select.text=Select +edit.bookmark.text=Add/Remove Bookmark +edit.bookmark.mnemonic=B +edit.bookmark.accelerator=ctrl B +edit.edit_note.text=Edit Note +edit.edit_note.mnemonic=N +edit.edit_note.accelerator=ctrl N +edit.select_next.text=Select Next +edit.select_next.mnemonic=N +edit.select_next.accelerator=ctrl RIGHT +edit.select_previous.text=Select Previous +edit.select_previous.mnemonic=P +edit.select_previous.accelerator=ctrl LEFT +edit.edit.text=Edit +edit.edit.mnemonics=U +edit.edit.accelerator=ctrl U +edit.locate.text=Find +edit.locate.mnemonics=F +edit.locate.accelerator=ctrl F +edit.highlight.text=Highlight +edit.highlight.mnemonics=H +edit.highlight.accelerator=ctrl H +view.text=View +view.mnemonic=V +view.zoom_out.text=Zoom out +view.zoom_out.mnemonic=O +view.zoom_out.accelerator=ctrl MINUS +view.zoom_in.text=Zoom in +view.zoom_in.mnemonic=I +view.zoom_in.accelerator=ctrl EQUALS +view.smaller_grid.text=Smaller grid +view.smaller_grid.mnemonic=S +view.grow_drawing_area.text=Grow drawing area +view.grow_drawing_area.mnemonic=G +view.clip_drawing_area.text=Clip drawing area +view.clip_drawing_area.mnemonic=C +view.larger_grid.text=Larger grid +view.larger_grid.mnemonic=L +view.hide_grid.text=Hide grid +view.hide_grid.mnemonic=H +view.change_laf.text=Change Look&Feel +view.change_laf.mnemonic=K +template.text=Template +template.mnemonic=T +collab.text=Collaboration +collab.mnemonic=C +collab.start_server.text=Start Server +collab.stop_server.text=Stop Server +collab.share_diagram.text=Share Diagram +collab.open_shared_diagram.text= Open Shared Diagram +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.show_haptics.text=Show Haptics Window +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.enable_log.text=Enable interaction log +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 +preferences.change_haptics.text=Change Haptic Device + + +help.text=Help +help.mnemonic=H +help.about.text=About +#help.about.mnemonic=A +help.license.text=License +help.license.mnemonic=L + + +no_arrow_string=None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/EditorTabbedPane.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,137 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.JTabbedPane; +import javax.swing.KeyStroke; + +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +/** + * + * The tabbed pane of the editor. On each tab a {@code DiagramPanel} is displayed. + * + */ +@SuppressWarnings("serial") +public class EditorTabbedPane extends JTabbedPane { + /** + * Creates a new {@code EditorTabbedPane} + * + * @param frame the frame when this tabbed pane will be placed + */ + public EditorTabbedPane(EditorFrame frame){ + setFocusTraversalKeysEnabled(false); + + 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(){ + public void actionPerformed(ActionEvent evt){ + NarratorFactory.getInstance().shutUp(); + } + }); + } + + /** + * 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) + return; + 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) + return null; + 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()); + } + + @Override + public DiagramPanel getComponentAt(int n){ + return (DiagramPanel)super.getComponent(n); + } + + /** + * The components in an {@code EditorTabbedPane} are all instances of {@code DiagramPanel}, which in turns + * holds an instance of {@code Diagram}. This utility methods retrieves the index of + * the {@code DiagramPanel} whose diagram has the same name than the {@code String} passed as argument. + * + * @param diagramName the name of the diagram to look for + * @return the index of the diagram named as {@code diagramName} or {@code -1} if + * such diagram doesn't exist + */ + public int getDiagramNameTabIndex(String diagramName){ + for(int i=0; i<getTabCount();i++){ + DiagramPanel dPanel = getComponentAt(i); + if(diagramName.equals(dPanel.getDiagram().getName())){ + return i; + } + } + return -1; + } + + /** + * The components in an {@code EditorTabbedPane} are all instances of {@code DiagramPanel}, which in turns + * hold an instance of {@code Diagram}. This method returns the index of the {@code DiagramPanel} + * whose diagram has been saved on the file system at the path specified as argument. + * + * @param path a path on the file system identifying the diagram + * @return the index of the {@code DiagramPanel} whose diagram has been saved on + * the file system in {@code path}, or {@code -1} if such diagram doesn't exist + */ + public int getPathTabIndex(String path){ + for(int i=0; i<getTabCount();i++){ + DiagramPanel dPanel = getComponentAt(i); + if(path.equals(dPanel.getFilePath())){ + return i; + } + } + return -1; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/ExtensionFilter.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,80 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.io.File; +import java.util.StringTokenizer; +import javax.swing.filechooser.FileFilter; + +/** + A file filter that accepts all files with a given set + of extensions. +*/ +public class ExtensionFilter extends FileFilter { + /** + Constructs an extension file filter. + @param description the description (e.g. "Woozle files") + @param extensions the accepted extensions (e.g. + new String[] { ".woozle", ".wzl" }) + */ + public ExtensionFilter(String description, + String[] extensions){ + this.description = description; + this.extensions = extensions; + } + + /** + Constructs an extension file filter. + @param description the description (e.g. "Woozle files") + @param extensions a list of '|'-separated accepted extensions + */ + public ExtensionFilter(String description, String extensions){ + this.description = description; + StringTokenizer tokenizer = new StringTokenizer( + extensions, "|"); + this.extensions = new String[tokenizer.countTokens()]; + for (int i = 0; i < this.extensions.length; i++) + this.extensions[i] = tokenizer.nextToken(); + } + + @Override + public boolean accept(File f){ + if (f.isDirectory()) return true; + String fname = f.getName().toLowerCase(); + for (int i = 0; i < extensions.length; i++) + if (fname.endsWith(extensions[i].toLowerCase())) + return true; + return false; + } + + @Override + public String getDescription(){ + return description; + } + + @Override + public String toString(){ + return description; + } + + private String description; + private String[] extensions; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/FileService.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,357 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Frame; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ResourceBundle; + +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; + +import uk.ac.qmul.eecs.ccmi.gui.filechooser.FileChooser; +import uk.ac.qmul.eecs.ccmi.gui.filechooser.FileChooserFactory; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/** + * A Utility class providing inner classes and interfaces for handling + * files. + */ +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(); + /** + * 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(); + + } + + /** + * A Save object encapsulates the stream and name of the file that the user selected for saving. + */ + public interface Save + { + /** + * Gets the output stream corresponding to the user selection. + * @return the output stream, or null if the user cancels the file selection task + */ + OutputStream getOutputStream(); + /** + * Gets the name of the file that the user selected. + * @return the file name, or null if the user cancels the file selection task + */ + String getName(); + /** + * Gets the path of the file that the user selected. + * @return the file path, or null if the user cancels the file selection task + */ + String getPath(); + } + + /** + * This class returns a FileService for opening and saving files, with either a JFileChooser or a + * SpeechFileChooser + */ + public static class ChooserService + { + /** + * Creates a new {@code ChooserService}. + * + * @param initialDirectory the directory displayed when the files service is displayed + */ + public ChooserService(File initialDirectory){ + useAccessible = Boolean.parseBoolean(PreferencesService.getInstance().get("use_accessible_filechooser", "true")); + fileChooser = FileChooserFactory.getFileChooser(useAccessible); + fileChooser.setCurrentDirectory(initialDirectory); + } + + /** + * Returns a {@code FileService.Open} out of a file chosen by the user. The user is prompted + * with either a normal or an accessible file choser. + * + * @param defaultDirectory the directory to open the file chooser, prompted to the user + * @param defaultFile the file selected when the file chooser is prompted to the user + * @param filter an {@code ExtensionFilter} to filter the filed displayed on the file chooser + * @param frame the frame where the file chooser is displayed + * @return an {@code FileService.Open} to handle the file selected by the user + * @throws IOException if the file chosen by the uses doesn't exist. + */ + public FileService.Open open(String defaultDirectory, String defaultFile, + ExtensionFilter filter, Frame frame) throws IOException { + 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); + } + } + + /** + * Returns a {@code FileService.Save} out of a file chosen by the user. The user is prompted + * with either a normal or an accessible file chooser. + * + * @param defaultDirectory the directory to open the file chooser, prompted to the user + * @param defaultFile the file selected when the file chooser is prompted to the user + * @param filter an {@code ExtensionFilter} to filter the filed displayed on the file chooser + * @param removeExtension the extension to be removed from the chosen file name. Use {@code null} for removing no extension + * @param addExtension the extension to be added to the chosen file name. + * @param currentTabs an array of already open files names. If the selected file matches any of these, then an + * Exception is thrown. If {@code null} is passed, then this parameter will be ignored and no check will be done. + * @return an {@code FileService.Save} to handle the file selected by the user + * @throws IOException if the file chosen by the uses doesn't exist or has a file name that's already + * in {@code currentTabs}. + */ + 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); + /* check the name against the names of already open tabs */ + if(currentTabs != null){ + 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 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) */ + 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; + } + + /** + * A file service which doesn't show any dialog to let + * the user choose which file to open or save. + */ + public static class DirectService { + /** + * Creates a {@code FileService.Open} out of the file passed as argument. + * + * @param file the file to open + * @return a new {@code FileService.Open} instance + * @throws IOException if {@code file} cannot be found + */ + public FileService.Open open(File file) throws IOException{ + return new OpenImpl(file); + } + + /** + * Creates a {@code FileService.Save} out of the file passed as argument. + * + * @param file the file to save + * @return a new {@code FileService.Save} instance + * @throws IOException if {@code file} cannot be found + */ + public FileService.Save save(File file) throws IOException{ + return new SaveImpl(file); + } + } + + private static class SaveImpl implements FileService.Save{ + public SaveImpl(File f) throws FileNotFoundException{ + if (f != null){ + path = f.getPath(); + name = getFileNameFromPath(path,false); + out = new BufferedOutputStream(new FileOutputStream(f)); + } + } + + @Override + public String getName() { return name; } + @Override + public String getPath() {return path; } + @Override + public OutputStream getOutputStream() { return out; } + + private String name; + private String path; + private OutputStream out; + } + + private static class OpenImpl implements FileService.Open{ + public OpenImpl(File f) throws FileNotFoundException{ + if (f != null){ + path = f.getPath(); + name = getFileNameFromPath(path,false); + in = new BufferedInputStream(new FileInputStream(f)); + } + } + + @Override + public String getName() { return name; } + @Override + public String getPath() { return path; } + @Override + public InputStream getInputStream() { return in; } + + private String path; + private String name; + private InputStream in; + } + + /** + * Edits the file path so that it ends in the desired + * extension. + * @param original the file to use as a starting point + * @param toBeRemoved the extension that is to be + * removed before adding the desired extension. Use + * null if nothing needs to be removed. + * @param desired the desired extension (e.g. ".png"), + * or a | separated list of extensions + * @return original if it already has the desired + * extension, or a new file with the edited file path + */ + public static String editExtension(String original, + String toBeRemoved, String desired){ + if (original == null) return null; + int n = desired.indexOf('|'); + if (n >= 0) desired = desired.substring(0, n); + String path = original; + if (!path.toLowerCase().endsWith(desired.toLowerCase())){ + if (toBeRemoved != null && path.toLowerCase().endsWith( + toBeRemoved.toLowerCase())) + path = path.substring(0, path.length() - toBeRemoved.length()); + path = path + desired; + } + return path; + } + + /** + * Returns the single file name from a file path + * + * @param path the path to extract the file name from + * @param keepExtension whether to keep the extension of the file + * in the returned string + * @return the name of the file identified by {@code path} + */ + public static String getFileNameFromPath(String path,boolean keepExtension){ + int index = path.lastIndexOf(System.getProperty("file.separator")); + String name; + if(index == -1) + name = path; + else + name = path.substring(index+1); + if(!keepExtension){ + index = name.lastIndexOf('.'); + if(index != -1) + name = name.substring(0, index); + } + return name; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Finder.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,184 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; +import java.util.Collection; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNode; + +/** + * + * A utility class providing static methods for searching either a node or an edge + * in a collection or array. + */ +public abstract class Finder { + /** + * Finds a node of a type in an array of nodes. The types should be all different + * from each other as only the first node encountered will be returned. If none of the nodes + * as the type passed as argument, {@code null} is returned. + * + * @param nodeType the type of the node to find. + * @param nodes the array to search for the node + * @return the first node with type {@code nodeType} or {@code null} if such node + * doesn't exist + */ + public static Node findNode(String nodeType,Node[] nodes){ + for(Node n : nodes){ + if(n.getType().equals(nodeType)){ + return n; + } + } + return null; + } + + /** + * Finds an edge of a {@code edgeType} type in an array of nodes. The types should be all different + * from each other as only the first edge encountered will be returned. If none of the edges + * as the type passed as argument, {@code null} is returned. + * + * @param edgeType the type of the edge to find + * @param edges the array to search for the edge + * @return the first edge with type {@code nodeType} or {@code null} if such edge + * doesn't exist + */ + public static Edge findEdge(String edgeType,Edge[] edges){ + for(Edge e : edges){ + if(e.getType().equals(edgeType)){ + return e; + } + } + return null; + } + + /** + * Finds a node with an id in a {@code Collection} of nodes. If none of the nodes + * has the id passed as argument, {@code null} is returned. + * + * @param id the id of the node to find + * @param collection the collection to search for the node + * @return the node with the specified id, or {@code null} if such node doesn't exist + */ + public static Node findNode(Long id, Collection<Node> collection){ + for(Node n : collection) + if(n.getId() == id) + return n; + return null; + } + + /** + * Finds a node containing a point {@code p} in a {@code Collection} of nodes. If none of the nodes + * contains the point, {@code null} is returned. + * + * @param p the point in a graphic environment + * @param collection the collection to search for the node + * @return the node containing {@code p}, or {@code null} if such node doesn't exist + */ + public static Node findNode(Point2D p, Collection<Node> collection){ + for (Node n : collection) + if (n.contains(p)) + return n; + return null; + } + + /** + * Finds an edge with an id in a {@code Collection} of edges. If none of the edges + * has the id passed as argument, {@code null} is returned. + * + * @param id the id of the edge to find + * @param collection the collection to search for the edge + * @return the edge with the specified id, or {@code null} if such edge doesn't exist + */ + public static Edge findEdge(Long id, Collection<Edge> collection){ + for(Edge e : collection) + if(e.getId() == id) + return e; + return null; + } + + /** + * Finds an edge containing a point {@code p} in a {@code Collection} of edges. If none of the edges + * contains the point, {@code null} is returned. + * + * @param p the point in a graphic environment + * @param collection the collection to search for the edge + * @return the edge containing {@code p}, or {@code null} if such edge doesn't exist + */ + public static Edge findEdge(Point2D p, Collection<Edge> collection){ + for (Edge e : collection) + if (e.contains(p)) + return e; + return null; + } + + /** + * Finds a element (node or edge) with an id in a {@code Collection} of diagram elements. If none of the elements + * has the id passed as argument, {@code null} is returned. + * + * @param id the id of the element to find + * @param collection the collection to search for the element + * @return the element with the specified id, or {@code null} if such element doesn't exist + */ + public static DiagramElement findElement(Long id, Collection<DiagramElement> collection){ + for(DiagramElement e : collection) + if(e.getId() == id) + return e; + return null; + } + + /** + * Finds a element (node or edge) with an identity hash code in a {@code Collection} of diagram elements. If none of the elements + * has the code passed as argument, {@code null} is returned. + * + * @param identityHashcode the identity hash code of the element to find + * @param collection the collection to search for the element + * @return the element with the specified identity hash code, or {@code null} if such element doesn't exist + * + * @see Object#hashCode() + */ + public static DiagramElement findElementByHashcode(long identityHashcode, Collection<DiagramElement> collection){ + for(DiagramElement de : collection){ + if(System.identityHashCode(de) == identityHashcode){ + return de; + } + } + return null; + } + + /** + * Returns the tree node whose path is described by the variable path + * where path contains the indexes returned by each node n of the + * path upon calling n.getParent().getChildAt(n). + * + * @param path the path for the node in the tree + * @param root the node where the path starts + * @return the node at the specified path, or {@code null} otherwise + */ + public static DiagramTreeNode findTreeNode(int[] path, DiagramTreeNode root){ + DiagramTreeNode retVal = root; + for(int i=0;i<path.length;i++){ + if(retVal.getChildCount() <= path[i]) + return null; + retVal = retVal.getChildAt(path[i]); + } + return retVal; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphElement.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,92 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Graphics2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + + +/** + * An interface implemented by {@code Node}, {@code Edge} and {@code Edge.InnerPoint}. It defines methods that + * all objects painted on a graph share. The interface is used mainly + * for convenience in treating the different types of object in a uniformly. + * + */ +public interface GraphElement { + /** + * Draw the graphic element on a canvas + * + * @param g2 the graphics object. Use {@code g2.draw()} to get things painted on the graph. + */ + public void draw(Graphics2D g2); + + /** + * Translates this graphic element on the graph + * + * @param p the starting point where the translation starts (normally where the user clicks + * with their mouse) + * @param dx the distance to translate along the x-axis + * @param dy the distance to translate along the y-axis + * @param source the source of the translate action + */ + public void translate(Point2D p, double dx, double dy, Object source); + + /** + * This method is to be called before translation or any other operation that changes the + * position of the graph element or any of its parts. + * + * @param p the starting point of the motion + * @param source the source of the motion action + */ + public void startMove(Point2D p, Object source); + + /** + * This method is to be called when the motion (e.g. a translation) is over. + * Note that for instance a translation might be composed on several calls to {@code translate} + * + * @param source the source of the motion action + */ + public void stopMove(Object source); + + /** + * Gets the bounding {@code Rectangle} of this graph element. + * + * @return a new {@code Rectangle} equals to the bounding {@code Rectangle} of this graph element + */ + public Rectangle2D getBounds(); + + /** + * Returns the point where an line, with a specified direction, would come in contact with the outline of this + * graph element. + * + * @param d the direction of the line + * @return a new point on the outline if this graph element where the line comes in contact with it + */ + public Point2D getConnectionPoint(Direction d); + + /** + * Tests if a specified {@code Point2D} is inside the boundary of this graph element. + * + * @param p the point to be tested + * @return {@code true} if the point is inside the boundary, {@code false} otherwise + */ + public boolean contains(Point2D p); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphPanel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,770 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.InputEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionListener; +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.network.Command; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * A panel to draw a graph + */ +@SuppressWarnings("serial") +public class GraphPanel extends JPanel{ + /** + * Constructs a graph. + * @param aDiagram a diagram to paint in the graph + * @param aToolbar a toolbar containing the node and edges prototypes for creating + * elements in the graph. + */ + + public GraphPanel(Diagram aDiagram, GraphToolbar aToolbar) { + grid = new Grid(); + gridSize = GRID; + grid.setGrid((int) gridSize, (int) gridSize); + zoom = 1; + toolbar = aToolbar; + setBackground(Color.WHITE); + wasMoving = false; + minBounds = null; + + this.model = aDiagram.getCollectionModel(); + synchronized(model.getMonitor()){ + edges = new LinkedList<Edge>(model.getEdges()); + nodes = new LinkedList<Node>(model.getNodes()); + } + setModelUpdater(aDiagram.getModelUpdater()); + + selectedElements = new HashSet<DiagramElement>(); + moveLockedElements = new HashSet<Object>(); + + toolbar.setEdgeCreatedListener(new innerEdgeListener()); + + /* ---- COLLECTION LISTENER ---- + * Adding a collection listener. This listener reacts at changes in the model + * by any source, and thus the graph itself. Basically it refreshes the graph + * and paints again all the nodes and edges. + */ + model.addCollectionListener(new CollectionListener(){ + @Override + public void elementInserted(final CollectionEvent e) { + DiagramElement element = e.getDiagramElement(); + if(element instanceof Node) + nodes.add((Node)element); + else + edges.add((Edge)element); + checkBounds(element,false); + if(e.getDiagramElement() instanceof Node && e.getSource().equals(DiagramEventSource.GRPH)){ + setElementSelected(e.getDiagramElement()); + dragMode = DRAG_NODE; + } + revalidate(); + repaint(); + } + @Override + public void elementTakenOut(final CollectionEvent e) { + DiagramElement element = e.getDiagramElement(); + if(element instanceof Node){ + if(nodePopup != null && nodePopup.getElement().equals(element)) + nodePopup.setVisible(false); + nodes.remove(element); + } + else{ + if(edgePopup != null && edgePopup.getElement().equals(element)) + edgePopup.setVisible(false); + edges.remove(element); + } + checkBounds(e.getDiagramElement(),true); + removeElementFromSelection(e.getDiagramElement()); + revalidate(); + repaint(); + } + @Override + public void elementChanged(final ElementChangedEvent e) { + /* we changed the position of an element and might need to update the boundaries */ + if(e.getChangeType().equals("stop_move")){ + checkBounds(e.getDiagramElement(),false); + } + revalidate(); + repaint(); + } + }); + /* --------------------------------------------------------------------------- */ + + /* ------------- MOUSE LISTENERS -------------------------------------------- + * For pressed and released mouse click and moved mouse + */ + addMouseListener(new MouseAdapter(){ + @Override + public void mousePressed(MouseEvent event){ + requestFocusInWindow(); + final Point2D mousePoint = new Point2D.Double( + (event.getX()+minX)/zoom, + (event.getY()+minY)/zoom + ); + boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; + Node n = Finder.findNode(mousePoint,nodes); + Edge e = Finder.findEdge(mousePoint,edges); + + Object tool = toolbar.getSelectedTool(); + /* - right click - */ + if((event.getModifiers() & InputEvent.BUTTON1_MASK) == 0) { + if(n != null){ + CCmIPopupMenu.NodePopupMenu pop = new CCmIPopupMenu.NodePopupMenu(n,GraphPanel.this,modelUpdater,selectedElements); + nodePopup = pop; + pop.show(GraphPanel.this, event.getX(), event.getY()); + }else if(e != null){ + if( e.contains(mousePoint)){ + Node extremityNode = e.getClosestNode(mousePoint,EDGE_END_MIN_CLICK_DIST); + if(extremityNode == null){ // click far from the attached nodes, only prompt with set name item + CCmIPopupMenu.EdgePopupMenu pop = new CCmIPopupMenu.EdgePopupMenu(e,GraphPanel.this,modelUpdater,selectedElements); + edgePopup = pop; + pop.show(GraphPanel.this, event.getX(), event.getY()); + }else{ // click near an attached nodes, prompt for name change, set end label and select arrow head + CCmIPopupMenu.EdgePopupMenu pop = new CCmIPopupMenu.EdgePopupMenu(e,extremityNode,GraphPanel.this,modelUpdater,selectedElements); + edgePopup = pop; + pop.show(GraphPanel.this, event.getX(), event.getY()); + } + } + }else { + return; + } + } + + /* - one click && palette == select - */ + else if (tool == null){ + if(n != null){ // node selected + if (isCtrl){ + addElementToSelection(n,false); + }else{ + setElementSelected(n); + } + dragMode = DRAG_NODE; + }else if (e != null){ // edge selected + if (isCtrl){ + addElementToSelection(e,false); + dragMode = DRAG_NODE; + }else{ + setElementSelected(e); + modelUpdater.startMove(e, mousePoint,DiagramEventSource.GRPH); + dragMode = DRAG_EDGE; + } + }else{ // nothing selected : make selection lasso + if (!isCtrl) + clearSelection(); + dragMode = DRAG_LASSO; + } + } + /* - one click && palette == node - */ + else { + /* click on an already existing node = select it*/ + if (n != null){ + if (isCtrl) + addElementToSelection(n,false); + else + setElementSelected(n); + dragMode = DRAG_NODE; + }else{ + Node prototype = (Node) tool; + Node newNode = (Node) prototype.clone(); + Rectangle2D bounds = newNode.getBounds(); + /* perform the translation from the origin */ + newNode.translate(new Point2D.Double(), mousePoint.getX() - bounds.getX(), + mousePoint.getY() - bounds.getY(),DiagramEventSource.NONE); + /* log stuff */ + iLog("insert node",""+((newNode.getId() == DiagramElement.NO_ID) ? "(no id)" : newNode.getId())); + /* insert the node into the model (no lock needed) */ + modelUpdater.insertInCollection(newNode,DiagramEventSource.GRPH); + } + } + + lastMousePoint = mousePoint; + mouseDownPoint = mousePoint; + repaint(); + } + + @Override + public void mouseReleased(MouseEvent event){ + final Point2D mousePoint = new Point2D.Double( + (event.getX()+minX)/zoom, + (event.getY()+minY)/zoom + ); + if(lastSelected != null){ + if(lastSelected instanceof Node || selectedElements.size() > 1){ // differentiate between translate and edge bending + if(wasMoving){ + iLog("move selected stop",mousePoint.getX()+" "+ mousePoint.getY()); + for(Object element : moveLockedElements){ + modelUpdater.stopMove((GraphElement)element,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 && selectedelements.size() = 1. Bending + if(wasMoving){ + iLog("bend edge stop",mousePoint.getX()+" "+ mousePoint.getY()); + if(moveLockedEdge != null){ + modelUpdater.stopMove(moveLockedEdge,DiagramEventSource.GRPH); + modelUpdater.yieldLock(moveLockedEdge, Lock.MOVE, new DiagramEventActionSource( + DiagramEventSource.GRPH, + Command.Name.BEND, + moveLockedEdge.getId(), + moveLockedEdge.getName())); + moveLockedEdge = null; + } + } + } + } + dragMode = DRAG_NONE; + wasMoving = false; + repaint(); + } + }); + + addMouseMotionListener(new MouseMotionAdapter(){ + public void mouseDragged(MouseEvent event){ + Point2D mousePoint = new Point2D.Double( + (event.getX()+minX)/zoom, + (event.getY()+minY)/zoom + ); + boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0; + + if (dragMode == DRAG_NODE){ + /* translate selected nodes (edges as well) */ + double dx = mousePoint.getX() - lastMousePoint.getX(); + double dy = mousePoint.getY() - lastMousePoint.getY(); + if(!wasMoving){ + wasMoving = true; + /* when the motion starts, we need to get the move-lock from the server */ + Iterator<DiagramElement> iterator = selectedElements.iterator(); + while(iterator.hasNext()){ + DiagramElement element = iterator.next(); + 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)); + iterator.remove(); + } + } + iLog("move selected start",mousePoint.getX()+" "+ mousePoint.getY()); + } + + for (DiagramElement selected : selectedElements){ + if(selected instanceof Node) + modelUpdater.translate((Node)selected, lastMousePoint, dx, dy,DiagramEventSource.GRPH); + else + modelUpdater.translate((Edge)selected, lastMousePoint, dx, dy,DiagramEventSource.GRPH); + } + } else if(dragMode == DRAG_EDGE){ + if(!wasMoving){ + wasMoving = true; + if(modelUpdater.getLock(lastSelected, + Lock.MOVE, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + Command.Name.BEND, + lastSelected.getId(), + lastSelected.getName())) + ){ + moveLockedEdge = (Edge)lastSelected; + }else{ + iLog("Could not get move lock for element",DiagramElement.toLogString(lastSelected)); + } + iLog("bend edge start",mousePoint.getX()+" "+ mousePoint.getY()); + } + if(moveLockedEdge != null) + modelUpdater.bend(moveLockedEdge, new Point2D.Double(mousePoint.getX(), mousePoint.getY()),DiagramEventSource.GRPH); + } else if (dragMode == DRAG_LASSO){ + double x1 = mouseDownPoint.getX(); + double y1 = mouseDownPoint.getY(); + double x2 = mousePoint.getX(); + double y2 = mousePoint.getY(); + Rectangle2D.Double lasso = new Rectangle2D.Double(Math.min(x1, x2), + Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2)); + for (Node n : GraphPanel.this.nodes){ + Rectangle2D bounds = n.getBounds(); + if(!isCtrl && !lasso.contains(bounds)){ + removeElementFromSelection(n); + } + else if (lasso.contains(bounds)){ + addElementToSelection(n,true); + } + } + if(selectedElements.size() != oldLazoSelectedNum){ + StringBuilder builder = new StringBuilder(); + for(DiagramElement de : selectedElements) + builder.append(DiagramElement.toLogString(de)).append(' '); + iLog("added by lazo",builder.toString()); + } + oldLazoSelectedNum = selectedElements.size(); + } + lastMousePoint = mousePoint; + } + }); + } + /* --------------------------------------------------------------------------- */ + + @Override + public void paintComponent(Graphics g){ + super.paintComponent(g); + paintGraph(g); + } + + /** + * Paints the graph on the graphics passed as argument. This function is called + * each time the component is painted. + * + * @see #paintComponent(Graphics) + * @param g the graphics object used to paint this graph + */ + public void paintGraph(Graphics g){ + Graphics2D g2 = (Graphics2D) g; + g2.translate(-minX, -minY); + g2.scale(zoom, zoom); + Rectangle2D bounds = getBounds(); + Rectangle2D graphBounds = getGraphBounds(); + if (!hideGrid) grid.draw(g2, new Rectangle2D.Double(minX, minY, + Math.max(bounds.getMaxX() / zoom, graphBounds.getMaxX()), + Math.max(bounds.getMaxY() / zoom, graphBounds.getMaxY()))); + + /* draw nodes and edges */ + for (Edge e : edges) + e.draw(g2); + for (Node n : nodes) + n.draw(g2); + + for(DiagramElement selected : selectedElements){ + if (selected instanceof Node){ + Rectangle2D grabberBounds = ((Node) selected).getBounds(); + drawGrabber(g2, grabberBounds.getMinX(), grabberBounds.getMinY()); + drawGrabber(g2, grabberBounds.getMinX(), grabberBounds.getMaxY()); + drawGrabber(g2, grabberBounds.getMaxX(), grabberBounds.getMinY()); + drawGrabber(g2, grabberBounds.getMaxX(), grabberBounds.getMaxY()); + } + else if (selected instanceof Edge){ + for(Point2D p : ((Edge)selected).getConnectionPoints()) + drawGrabber(g2, p.getX(), p.getY()); + } + } + + if (dragMode == DRAG_LASSO){ + Color oldColor = g2.getColor(); + g2.setColor(GRABBER_COLOR); + double x1 = mouseDownPoint.getX(); + double y1 = mouseDownPoint.getY(); + double x2 = lastMousePoint.getX(); + double y2 = lastMousePoint.getY(); + Rectangle2D.Double lasso = new Rectangle2D.Double(Math.min(x1, x2), + Math.min(y1, y2), Math.abs(x1 - x2) , Math.abs(y1 - y2)); + g2.draw(lasso); + g2.setColor(oldColor); + repaint(); + } + } + + /** + * Draws a single "grabber", a filled square + * @param g2 the graphics context + * @param x the x coordinate of the center of the grabber + * @param y the y coordinate of the center of the grabber + */ + static void drawGrabber(Graphics2D g2, double x, double y){ + final int SIZE = 5; + Color oldColor = g2.getColor(); + g2.setColor(GRABBER_COLOR); + g2.fill(new Rectangle2D.Double(x - SIZE / 2, y - SIZE / 2, SIZE, SIZE)); + g2.setColor(oldColor); + } + + @Override + public Dimension getPreferredSize(){ + Rectangle2D graphBounds = getGraphBounds(); + return new Dimension((int) (zoom * graphBounds.getMaxX()), + (int) (zoom * graphBounds.getMaxY())); + } + + /** + * Changes the zoom of this panel. The zoom is 1 by default and is multiplied + * by sqrt(2) for each positive stem or divided by sqrt(2) for each negative + * step. + * @param steps the number of steps by which to change the zoom. A positive + * value zooms in, a negative value zooms out. + */ + public void changeZoom(int steps){ + final double FACTOR = Math.sqrt(2); + for (int i = 1; i <= steps; i++) + zoom *= FACTOR; + for (int i = 1; i <= -steps; i++) + zoom /= FACTOR; + revalidate(); + repaint(); + } + + /** + * Changes the grid size of this panel. The zoom is 10 by default and is + * multiplied by sqrt(2) for each positive stem or divided by sqrt(2) for + * each negative step. + * @param steps the number of steps by which to change the zoom. A positive + * value zooms in, a negative value zooms out. + */ + public void changeGridSize(int steps){ + final double FACTOR = Math.sqrt(2); + for (int i = 1; i <= steps; i++) + gridSize *= FACTOR; + for (int i = 1; i <= -steps; i++) + gridSize /= FACTOR; + grid.setGrid((int) gridSize, (int) gridSize); + repaint(); + } + + private void addElementToSelection(DiagramElement element, boolean byLasso){ + /* if not added to selected elements by including it in the lasso, the element is moved * + * to the back of the collection so that it will be painted on the top on the next refresh */ + if(!byLasso) + if(element instanceof Node){ + /* put the node in the last position so that it will be drawn on the top */ + nodes.remove(element); + nodes.add((Node)element); + iLog("addeded node to selected",DiagramElement.toLogString(element)); + }else{ + /* put the edge in the last position so that it will be drawn on the top */ + edges.remove(element); + edges.add((Edge)element); + iLog("addeded edge to selected",DiagramElement.toLogString(element)); + } + if(selectedElements.contains(element)){ + lastSelected = element; + return; + } + lastSelected = element; + selectedElements.add(element); + return; + } + + private void removeElementFromSelection(DiagramElement element){ + if (element == lastSelected){ + lastSelected = null; + } + if(selectedElements.contains(element)){ + selectedElements.remove(element); + } + } + + private void setElementSelected(DiagramElement element){ + /* clear the selection */ + selectedElements.clear(); + lastSelected = element; + selectedElements.add(element); + if(element instanceof Node){ + nodes.remove(element); + nodes.add((Node)element); + iLog("node selected",DiagramElement.toLogString(element)); + }else{ + edges.remove(element); + edges.add((Edge)element); + iLog("edge selected",DiagramElement.toLogString(element)); + } + } + + private void clearSelection(){ + iLog("selection cleared",""); + selectedElements.clear(); + lastSelected = null; + } + + /** + * Sets the value of the hideGrid property + * @param newValue true if the grid is being hidden + */ + public void setHideGrid(boolean newValue){ + hideGrid = newValue; + repaint(); + } + + /** + * Gets the value of the hideGrid property + * @return true if the grid is being hidden + */ + public boolean getHideGrid(){ + return hideGrid; + } + + /** + * Gets the smallest rectangle enclosing the graph + * @return the bounding rectangle + */ + public Rectangle2D getMinBounds() { return minBounds; } + + /** + * Sets the smallest rectangle enclosing the graph + * @param newValue the bounding rectangle + */ + public void setMinBounds(Rectangle2D newValue) { minBounds = newValue; } + + /** + * Returns the smallest rectangle enclosing the graph, that is all the nodes + * and all the edges in the diagram. + * + * @return the bounding rectangle + */ + public Rectangle2D getGraphBounds(){ + Rectangle2D r = minBounds; + for (Node n : nodes){ + Rectangle2D b = n.getBounds(); + if (r == null) r = b; + else r.add(b); + } + for (Edge e : edges){ + r.add(e.getBounds()); + } + return r == null ? new Rectangle2D.Double() : new Rectangle2D.Double(r.getX(), r.getY(), + r.getWidth() + Node.SHADOW_GAP + Math.abs(minX), r.getHeight() + Node.SHADOW_GAP + Math.abs(minY)); + } + + /** + * Sets the model updater for this graph. The model updater is used + * to make changes to the diagram (e.g. adding, removing, renaming nodes and edges) + * + * @param modelUpdater the model updater for this graph panel + */ + public void setModelUpdater(DiagramModelUpdater modelUpdater){ + this.modelUpdater = modelUpdater; + } + + private void iLog(String action,String args){ + InteractionLog.log("GRAPH",action,args); + } + + private void checkBounds(DiagramElement de, boolean wasRemoved){ + GraphElement ge; + if(de instanceof Node) + ge = (Node)de; + else + ge = (Edge)de; + if(wasRemoved){ + if(ge == top){ + top = null; + minY = 0; + Rectangle2D bounds; + for(Edge e : edges){ + bounds = e.getBounds(); + if(bounds.getY() < minY){ + top = e; + minY = bounds.getY(); + } + } + for(Node n : nodes){ + bounds = n.getBounds(); + if(bounds.getY() < minY){ + top = n; + minY = bounds.getY(); + } + } + } + if(ge == left){ + minX = 0; + left = null; + synchronized(model.getMonitor()){ + Rectangle2D bounds; + for(Edge e : model.getEdges()){ + bounds = e.getBounds(); + if(bounds.getX() < minX){ + left = e; + minX = bounds.getX(); + } + } + for(Node n : model.getNodes()){ + bounds = n.getBounds(); + if(bounds.getX() < minX){ + left = n; + minX = bounds.getX(); + } + } + } + } + }else{ // was added or translated + Rectangle2D bounds = ge.getBounds(); + if(top == null){ + if(bounds.getY() < 0){ + top = ge; + minY = bounds.getY(); + } + }else if(ge == top){ //the top-most has been translated recalculate the new top-most, as itf it were deleted + checkBounds(de, true); + }else if(bounds.getY() < top.getBounds().getY()){ + top = ge; + minY = bounds.getY(); + } + + if(left == null){ + if(bounds.getX() < 0){ + left = ge; + minX = bounds.getX(); + } + }else if(ge == left){ + checkBounds(de,true);//the left-most has been translated recalculate the new left-most, as if it were deleted + } + else if(bounds.getX() < left.getBounds().getX()){ + left = ge; + minX = bounds.getX(); + } + } + } + + private class innerEdgeListener implements GraphToolbar.EdgeCreatedListener { + @Override + public void edgeCreated(Edge e) { + ArrayList<DiagramNode> nodesToConnect = new ArrayList<DiagramNode>(selectedElements.size()); + + for(DiagramElement element : selectedElements){ + if(element instanceof Node){ + if(!modelUpdater.getLock(element, + Lock.MUST_EXIST, + new DiagramEventActionSource(DiagramEventSource.GRPH, + Command.Name.SELECT_NODE_FOR_EDGE_CREATION, + element.getId(), + element.getName()))){ + /* unlock the nodes locked so far */ + yieldLocks(nodesToConnect); + /* notify user */ + JOptionPane.showMessageDialog(GraphPanel.this, + ResourceBundle.getBundle(EditorFrame.class.getName()).getString("dialog.lock_failure.no_edge_creation")); + return; + } + nodesToConnect.add((Node)element); + } + } + try { + e.connect(nodesToConnect); + modelUpdater.insertInCollection(e,DiagramEventSource.GRPH); + /* release the must-exist lock on nodes now that the edge is created */ + yieldLocks(nodesToConnect); + } catch (ConnectNodesException cnEx) { + JOptionPane.showMessageDialog(GraphPanel.this, + cnEx.getLocalizedMessage(), + ResourceBundle.getBundle(EditorFrame.class.getName()).getString("dialog.error.title"), + JOptionPane.ERROR_MESSAGE); + iLog("insert edge error",cnEx.getMessage()); + } + } + + /* release all locks */ + private void yieldLocks(ArrayList<DiagramNode> nodesToConnect){ + for(DiagramNode node : nodesToConnect){ + modelUpdater.yieldLock(node, + Lock.MUST_EXIST, + new DiagramEventActionSource( + DiagramEventSource.GRPH, + Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION, + node.getId(), + node.getName()) + ); + } + } + } + + private List<Edge> edges; + private List<Node> nodes; + private DiagramModelUpdater modelUpdater; + private CollectionModel<Node,Edge> model; + + private Grid grid; + private GraphToolbar toolbar; + private CCmIPopupMenu.NodePopupMenu nodePopup; + private CCmIPopupMenu.EdgePopupMenu edgePopup; + + private double zoom; + private double gridSize; + private boolean hideGrid; + private boolean wasMoving; + + private GraphElement top; + private GraphElement left; + private double minX; + private double minY; + + private DiagramElement lastSelected; + private Edge moveLockedEdge; + private Set<DiagramElement> selectedElements; + private Set<Object> moveLockedElements; + + private Point2D lastMousePoint; + private Point2D mouseDownPoint; + private Rectangle2D minBounds; + private int dragMode; + + private int oldLazoSelectedNum; + + /* button is not down, mouse motion will habe no effects */ + private static final int DRAG_NONE = 0; + /* one or more nodes (and eventually some edges) have been selected, mouse motion will result in a translation */ + private static final int DRAG_NODE = 1; + /* one edge has been selected, mouse motion will result in an edge bending */ + private static final int DRAG_EDGE = 2; + /* mouse button down but nothing selected, mouse motion will result in a lasso */ + private static final int DRAG_LASSO = 3; // multiple selection + + private static final int GRID = 10; + private static final double EDGE_END_MIN_CLICK_DIST = 10; + + public static final Color GRABBER_COLOR = new Color(0,128,255); +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/GraphToolbar.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,249 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.geom.AffineTransform; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.Enumeration; +import java.util.ResourceBundle; + +import javax.swing.ButtonGroup; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; + + +/** + A Toolbar that contains node and edge prototype icons. By using the toolbar + the user can create nodes and edges in the diagram out of clonation from the prototype. +*/ +@SuppressWarnings("serial") +public class GraphToolbar extends JToolBar { + /** + * Constructs a tool bar with no icons. + * + * @param diagram the diagram this toolbar is related to + */ + public GraphToolbar(Diagram diagram){ + /* creates icon for select button */ + Icon icon = new Icon(){ + public int getIconHeight() { return BUTTON_SIZE; } + public int getIconWidth() { return BUTTON_SIZE; } + public void paintIcon(Component c, Graphics g, + int x, int y){ + Graphics2D g2 = (Graphics2D)g; + GraphPanel.drawGrabber(g2, x + OFFSET, y + OFFSET); + GraphPanel.drawGrabber(g2, x + OFFSET, y + BUTTON_SIZE - OFFSET); + GraphPanel.drawGrabber(g2, x + BUTTON_SIZE - OFFSET, y + OFFSET); + GraphPanel.drawGrabber(g2, x + BUTTON_SIZE - OFFSET, y + BUTTON_SIZE - OFFSET); + } + }; + /* add selection button */ + ResourceBundle resources = + ResourceBundle.getBundle(EditorFrame.class.getName()); + String text = resources.getString("grabber.text"); + selectButton = new NodeButton(null,icon); + selectButton.setToolTipText(text); + nodeButtonsGroup = new ButtonGroup(); + nodeButtonsGroup.add(selectButton); + add(selectButton); + + /* add diagram buttons to the toolbar */ + Node[] nodeTypes = diagram.getNodePrototypes(); + for (int i = 0; i < nodeTypes.length; i++){ + text = nodeTypes[i].getType(); + add(nodeTypes[i], text ); + } + + /* select the select-button as default */ + nodeButtonsGroup.setSelected(selectButton.getModel(), true); + + /* separate node buttons from edge buttons */ + addSeparator(); + + /* add diagram edges to the toolbar */ + Edge[] edgeTypes = diagram.getEdgePrototypes(); + for (int i = 0; i < edgeTypes.length; i++){ + text = edgeTypes[i].getType(); + add(edgeTypes[i], text ); + } + } + + /** + Gets the node prototype that is associated with + the currently selected button + @return a {@code Node} prototype + */ + public Node getSelectedTool() { + @SuppressWarnings("rawtypes") + Enumeration elements = nodeButtonsGroup.getElements(); + while (elements.hasMoreElements()) { + NodeButton b = (NodeButton)elements.nextElement(); + if (b.isSelected()) { + /* switch back to the select-button */ + nodeButtonsGroup.setSelected(selectButton.getModel(), true); + return b.getNode(); + } + } + /* getting here means the selection button is selected */ + return null; + } + + /** + Adds a node to the tool bar. + @param n the node to add + @param tip the tool tip appearing when hovering on this edge button + */ + public void add(final Node n, String tip){ + Icon icon = new Icon(){ + public int getIconHeight() { return BUTTON_SIZE; } + public int getIconWidth() { return BUTTON_SIZE; } + public void paintIcon(Component c, Graphics g, + int x, int y){ + double width = n.getBounds().getWidth(); + double height = n.getBounds().getHeight(); + Graphics2D g2 = (Graphics2D)g; + double scaleX = (BUTTON_SIZE - OFFSET)/ width; + double scaleY = (BUTTON_SIZE - OFFSET)/ height; + double scale = Math.min(scaleX, scaleY); + + AffineTransform oldTransform = g2.getTransform(); + g2.translate(x, y); + g2.translate(OFFSET/2*scaleX,OFFSET/2*scaleY); + g2.scale(scale, scale); + g2.setColor(Color.black); + n.draw(g2); + g2.setTransform(oldTransform); + } + }; + + NodeButton button = new NodeButton(n, icon); + button.setToolTipText(tip); + + add(button); + nodeButtonsGroup.add(button); + } + + /** + Adds an edge to the tool bar. + @param e the edge to add + @param tip the tool tip appearing when hovering on this edge button + */ + public void add(final Edge e, String tip){ + Icon icon = new Icon(){ + public int getIconHeight() { return BUTTON_SIZE; } + public int getIconWidth() { return BUTTON_SIZE; } + public void paintIcon(Component c, Graphics g, + int x, int y){ + Graphics2D g2 = (Graphics2D)g; + /* create two points */ + Point2D p = new Point2D.Double(); + Point2D q = new Point2D.Double(); + p.setLocation(OFFSET, OFFSET); + q.setLocation(BUTTON_SIZE - OFFSET, BUTTON_SIZE - OFFSET); + + Line2D line = new Line2D.Double(p,q); + Rectangle2D bounds = new Rectangle2D.Double(); + bounds.add(line.getBounds2D()); + + double width = bounds.getWidth(); + double height = bounds.getHeight(); + double scaleX = (BUTTON_SIZE - OFFSET)/ width; + double scaleY = (BUTTON_SIZE - OFFSET)/ height; + double scale = Math.min(scaleX, scaleY); + + AffineTransform oldTransform = g2.getTransform(); + g2.translate(x, y); + g2.scale(scale, scale); + g2.translate(Math.max((height - width) / 2, 0), Math.max((width - height) / 2, 0)); + + g2.setColor(Color.black); + g2.setStroke(e.getStyle().getStroke()); + g2.draw(line); + g2.setTransform(oldTransform); + } + }; + final JButton button = new JButton(icon); + button.setToolTipText(tip); + button.setFocusable(false); + + button.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent evt) { + edgeCreatedListener.edgeCreated((Edge)e.clone()); + }}); + add(button); + } + + /** + * Sets the {@code EdgeCreatedListener} for this toolbar. Any previous set listener + * will be overwritten. + * + * @param edgeCreatedListener the new {@code EdgeCreatedListener} for this toolbar + */ + public void setEdgeCreatedListener(EdgeCreatedListener edgeCreatedListener){ + this.edgeCreatedListener = edgeCreatedListener; + } + + private class NodeButton extends JToggleButton{ + public NodeButton(Node node, Icon icon){ + super(icon); + setFocusable(false); + this.node = node; + } + + public Node getNode(){ + return node; + } + Node node; + } + + /** + * The listener interface receiving events when the user clicks on + * an {@code Edge} button. Unlike {@code Node} buttons which are {@code JTobbleButton} + * objects to select the {@code Node} returned by {@code getSelectedTool()}, + * the {@code Edge} buttons just trigger the registered listener with an immediate effect. + */ + public interface EdgeCreatedListener { + /** + * Invoked when an {@code Edge} button is pressed. + * @param e the {@code Edge} related to the pressed button + */ + void edgeCreated(Edge e); + } + + private ButtonGroup nodeButtonsGroup; + private EdgeCreatedListener edgeCreatedListener; + private NodeButton selectButton; + + private static final int BUTTON_SIZE = 30; + private static final int OFFSET = 5; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Grid.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,134 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RectangularShape; + +/** + A grid to which nodes can be "snapped". The + snapping operation moves a point to the nearest grid point. +*/ +public class Grid +{ + /** + Constructs a grid with no grid points. + */ + public Grid() + { + setGrid(0, 0); + } + + /** + Sets the grid point distances in x- and y-direction + @param x the grid point distance in x-direction + @param y the grid point distance in y-direction + */ + public void setGrid(double x, double y) + { + gridx = x; + gridy = y; + } + + /** + Draws this grid inside a rectangle. + @param g2 the graphics context + @param bounds the bounding rectangle + */ + public void draw(Graphics2D g2, Rectangle2D bounds) + { + Color PALE_BLUE = new Color(0.9F, 0.8F, 0.9F); + Color oldColor = g2.getColor(); + g2.setColor(PALE_BLUE); + Stroke oldStroke = g2.getStroke(); + for (double x = bounds.getX(); x < bounds.getMaxX(); x += gridx) + g2.draw(new Line2D.Double(x, bounds.getY(), x, bounds.getMaxY())); + for (double y = bounds.getY(); y < bounds.getMaxY(); y += gridy) + g2.draw(new Line2D.Double(bounds.getX(), y, bounds.getMaxX(), y)); + g2.setStroke(oldStroke); + g2.setColor(oldColor); + } + + /** + Snaps a point to the nearest grid point + @param p the point to snap. After the call, the + coordinates of p are changed so that p falls on the grid. + */ + public void snap(Point2D p) + { + double x; + if (gridx == 0) + x = p.getX(); + else + x = Math.round(p.getX() / gridx) * gridx; + double y; + if (gridy == 0) + y = p.getY(); + else + y = Math.round(p.getY() / gridy) * gridy; + + p.setLocation(x, y); + } + + /** + Snaps a rectangle to the nearest grid points + @param r the rectangle to snap. After the call, the + coordinates of r are changed so that all of its corners + falls on the grid. + */ + public void snap(RectangularShape r) + { + double x; + double w; + w = r.getWidth(); + if (gridx == 0) + { + x = r.getX(); + } + else + { + x = Math.round(r.getX() / gridx) * gridx; +// w = Math.ceil(r.getWidth() / (2 * gridx)) * (2 * gridx); + } + double y; + double h; + h = r.getHeight(); + if (gridy == 0) + { + y = r.getY(); + } + else + { + y = Math.round(r.getY() / gridy) * gridy; +// h = Math.ceil(r.getHeight() / (2 * gridy)) * (2 * gridy); + } + + r.setFrame(x, y, w, h); + } + + private double gridx; + private double gridy; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/HapticKindle.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,286 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ResourceBundle; + +import javax.swing.SwingUtilities; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.haptics.HapticListener; +import uk.ac.qmul.eecs.ccmi.haptics.HapticListenerThread; +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.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * + * An instance of HapticListener for the diagram editor. By this class visual diagrams + * can be manipulated by an haptic device. This class extends the {@code Thread} class, + * and can therefore be run on a separate thread listening to the haptic commands coming from the thread + * managing the haptic device. The commands affecting the Swing components will + * be queued for execution on the code Event Dispatching Thread event queue. + * + */ +public class HapticKindle extends HapticListenerThread { + + public HapticKindle(){ + super(); + cmdImpl = new CommandImplementation(); + /* unselect always ends up to the same instruction. Therefore don't create a new runnable * + * each time the command is issued, but rather keep and reuse always the same class */ + unselectRunnable = new Runnable(){ + @Override + public void run(){ + cmdImpl.executeCommand(HapticListenerCommand.UNSELECT, 0, 0, 0, 0, 0); + } + }; + } + + /** + * 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 HapticListenerThread#executeCommand(HapticListenerCommand, int, double, double, double, double) + */ + @Override + public void executeCommand(final HapticListenerCommand cmd, final int ID, final double x, final double y, final double startX, final double startY) { + switch(cmd){ + case PLAY_ELEMENT_SOUND : + case PLAY_ELEMENT_SPEECH : + case SELECT : + case INFO : + case ERROR : + SwingUtilities.invokeLater(new Runnable(){ + public void run(){ + cmdImpl.executeCommand(cmd, ID, x, y, startX, startY); + } + }); + break; + case UNSELECT : + SwingUtilities.invokeLater(unselectRunnable); + break; + case PICK_UP : + 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(){ + cmdImpl.executeCommand(cmd, ID, x, y, startX, startY); + } // run() + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + break; + + case PLAY_SOUND : + cmdImpl.executeCommand(cmd, ID, x, y, startX, startY); // not in the Event Dispatching Thread + break; + } + + } + + /** + * Returns the delegate inner implementation of the {@code HapticListener} commands. + * When called directly from the returned {@code HapticListener} the commands won't be executed on a separate thread. + * + * @return the {@code HapticListener} command implementation + */ + @Override + public HapticListener getNonRunnableListener(){ + return cmdImpl; + } + + + private Runnable unselectRunnable; + private CommandImplementation cmdImpl; + private static String INTERACTION_LOG_SOURCE = "HAPTIC"; + + /* An inner class with the implementation of all the commands. HapticKindle runs * + * on its own thread and delegates the real commands implementation to this class */ + private static class CommandImplementation implements HapticListener{ + @Override + public void executeCommand(HapticListenerCommand cmd, int ID, double x, + double y, double startX, double startY) { + final EditorFrame frame = DiagramEditorApp.getFrame(); + switch(cmd){ + case PLAY_ELEMENT_SOUND : { + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + /* can be null if the tab has been switched or closed in the meantime */ + if(de == null) + return; + SoundFactory.getInstance().play(de.getSound()); + }break; + case PLAY_ELEMENT_SPEECH : { + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + if(de == null) + return; + SoundFactory.getInstance().play(de.getSound()); + NarratorFactory.getInstance().speak(de.getName()); + iLog("touch",((de instanceof Node) ? "node " : "edge ")+de.getName()); + }break; + case SELECT : { + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + if(de == null) + return; + frame.selectHapticHighligh(de); + }break; + case UNSELECT : { + if((frame == null)||(frame.getActiveTab() == null)) + return; + frame.selectHapticHighligh(null); + }break; + case MOVE : { + /* when this block is executed we already have the lock * + * on the element from the PICK_UP command execution */ + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + if(de == null) + return; + DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater(); + if(de instanceof Node){ + Node n = (Node)de; + Rectangle2D bounds = n.getBounds(); + Point2D.Double p = new Point2D.Double(bounds.getCenterX(),bounds.getCenterY()); + double dx = x - p.getX(); + double dy = y - p.getY(); + 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()) + .append(' ').append(p.getY()); + iLog("move node start",builder.toString()); + builder = new StringBuilder(); + builder.append(DiagramElement.toLogString(n)).append(' ') + .append(x).append(' ').append(y); + iLog("move node end",builder.toString()); + }else{ + Edge e = (Edge)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(); + + StringBuilder builder = new StringBuilder(); + builder.append(DiagramElement.toLogString(e)).append(' ').append(startX) + .append(' ').append(startY); + iLog("bend edge start",builder.toString()); + builder = new StringBuilder(); + builder.append(DiagramElement.toLogString(e)).append(' ') + .append(x).append(' ').append(y); + iLog("bend edge end",builder.toString()); + } + 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() + )); + SoundFactory.getInstance().play(SoundEvent.HOOK_OFF); + }break; + case INFO : { + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + if(de == null) + return; + SoundFactory.getInstance().stop(); + NarratorFactory.getInstance().speak(de.detailedSpokenText()); + iLog("request detailed info",((de instanceof Node) ? "node " : "edge ")+de.getName()); + }break; + case PLAY_SOUND : { + switch(HapticListenerCommand.Sound.fromInt(ID) ){ + case MAGNET_OFF : + SoundFactory.getInstance().play(SoundEvent.MAGNET_OFF); + iLog("sticky mode off",""); + break; + case MAGNET_ON : + SoundFactory.getInstance().play(SoundEvent.MAGNET_ON); + iLog("sticky mode on",""); + break; + case DRAG : SoundFactory.getInstance().play(SoundEvent.DRAG); + break; + } + }break; + case PICK_UP :{ + if((frame == null)||(frame.getActiveTab() == null)) + return; + CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel(); + DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements()); + if(de == null) + return; + DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater(); + if(!modelUpdater.getLock(de, + Lock.MOVE, + new DiagramEventActionSource(DiagramEventSource.HAPT, de instanceof Edge ? Command.Name.TRANSLATE_EDGE : Command.Name.TRANSLATE_NODE ,de.getId(),de.getName()))){ + iLog("Could not get lock on element for motion", DiagramElement.toLogString(de)); + NarratorFactory.getInstance().speak("Object is being moved by another user"); + return; + } + frame.hPickUp(de); + SoundFactory.getInstance().play(SoundEvent.HOOK_ON); + iLog("hook on",""); + }break; + case ERROR : { + if((frame == null)||(frame.getActiveTab() == null)) + return; + frame.backupOpenDiagrams(); + NarratorFactory.getInstance().speak(ResourceBundle.getBundle(EditorFrame.class.getName()).getString("speech.haptic_device_crashed")); + }break; + } + } + + private void iLog(String action, String args){ + InteractionLog.log(INTERACTION_LOG_SOURCE,action,args); + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/HapticTrigger.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,86 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionListener; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.haptics.HapticsFactory; + +/** + * A CollectionListener that updates the haptic scene upon insertion deletion and + * movement of nodes and edges in the diagram. + * + * + */ +public class HapticTrigger implements CollectionListener { + + @Override + public void elementInserted(CollectionEvent evt) { + 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),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(),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),source.getDiagramName()); + }else{//edge + Edge e = (Edge)evt.getDiagramElement(); + 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()) || "remove_node".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(), + source.getDiagramName()); + }else{ + Node n = (Node)evt.getDiagramElement(); + HapticsFactory.getInstance().moveNode( + n.getBounds().getCenterX(), + n.getBounds().getCenterY(), + System.identityHashCode(n), + source.getDiagramName() + ); + } + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/LineStyle.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,74 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.BasicStroke; +import java.awt.Stroke; + +/** + * Defines the possible values for the stroke of lines painted on the graph when drawing an {@link Edge}. + * It can be <i>Solid</i>, <i>Dotted</i> or <i>Dashed</i>. + * + */ +public enum LineStyle { + Solid(new BasicStroke(),0xFFFF), + Dotted(new BasicStroke(1.0f, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 0.0f, + new float[]{1.0f,3.0f}, + 0.0f),0xAAAA), + Dashed(new BasicStroke(1.0f, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 0.0f, + new float[]{5.0f,5.0f}, + 0.0f),0xF0F0); + + 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 <a href="http://www.opengl.org/sdk/docs/man/xhtml/glLineStipple.xml">glLineStipple</a> + * + * @return an int with the bit representation of the stipple pattern + */ + public int getStipplePattern(){ + return stipplePattern; + } + + private Stroke stroke; + private int stipplePattern; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Lock.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,67 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +/** + * + * An enum that defines the possible locks that can be granted on a shared diagram. + * + */ +public enum Lock { + /** + * The element will be deleted, therefore no other operation on the same element is allowed to any other user. + */ + DELETE, + /** + * The element will be renamed, therefore no renaming of the same element is allowed to any other user. + */ + NAME, + /** + * The node properties will be edit, therefore no properties or modifiers editing + * on the same node will be allowed to any other user. + */ + PROPERTIES, + /** + * The edge end is being edited (label or arrow head), therefore no end editing + * on the same edge will be allowed to any other user. + */ + EDGE_END, + /** + * The element is being moved, therefore no move on the same element will be allowed to any other user. + */ + MOVE, + /** + * The notes of the tree node will be edited, therefore no notes editing + * on the same tree node will be allowed to any other user. + */ + NOTES, + /** + * The bookmarks of the tree node will be edited, therefore no bookmarks editing + * on the same tree node will be allowed to any other user. + */ + BOOKMARK, + /** + * The element cannot be deleted by other users. + */ + MUST_EXIST, + /** + * {@code null} value. + */ + NONE +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/LoopComboBox.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,89 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.event.ItemEvent; +import java.awt.event.KeyEvent; +import java.util.Vector; + +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; + +/** + * A ComboBox component which overrides the default behaviour when selecting items by the keyboard + * up and down arrow keys. When the top is reached, if the user presses the up arrow key + * instead of blocking on the first item, the LoopComboBox loops forward to the last item. Likewise, + * when the bottom is reached and the user presses the down arrow key the LoopComboBox + * loops back to the first item. + * + */ +@SuppressWarnings("serial") +public class LoopComboBox extends JComboBox { + public LoopComboBox(){ + super(); + } + + public LoopComboBox(Object[] items){ + super(items); + } + + public LoopComboBox(ComboBoxModel aModel){ + super(aModel); + } + + public LoopComboBox(Vector<?> items){ + super(items); + } + + @Override + public void processKeyEvent(KeyEvent e) { + if(dataModel.getSize() == 0){ + super.processKeyEvent(e); + }else{ + if(e.getKeyCode() == KeyEvent.VK_DOWN + && e.getID()==KeyEvent.KEY_PRESSED + && getSelectedIndex() == getItemCount()-1){ + setSelectedIndex(0); + if(getItemCount() == 1) + fireOneItemStateChanged(); + }else if(e.getKeyCode() == KeyEvent.VK_UP + && e.getID()==KeyEvent.KEY_PRESSED + && getSelectedIndex() == 0){ + setSelectedIndex(getItemCount()-1); + if(getItemCount() == 1) + fireOneItemStateChanged(); + }else + super.processKeyEvent(e); + } + } + + /* + * when the comboBox has only one item the ItemStateChanged listeners ain't fired by default. + * This behaviour has to be forced in order to have the item label to be spoken out by + * the narrator, in spite of the item number . + */ + private void fireOneItemStateChanged(){ + fireItemStateChanged(new ItemEvent( + this, + ItemEvent.ITEM_STATE_CHANGED, + getSelectedItem(), + ItemEvent.SELECTED + )); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/LoopSpinnerNumberModel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,60 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import javax.swing.SpinnerNumberModel; + +/** + * A SpinnerNumberModel which overrides the default behaviour when selecting items by the keyboard + * up and down arrow keys. + * When the maximum value is reached, a call to getNextValue() + * will return the minimum value, instead of returning null. Likewise, + * when the minimum value is reached, a call to getPreviousValue() will return the maximum value + * instead of returning null. + * + * + */ +@SuppressWarnings("serial") +public class LoopSpinnerNumberModel extends SpinnerNumberModel { + public LoopSpinnerNumberModel(int value, int minimum, int maximum){ + super(value,minimum,maximum,1); + } + + @Override + public Object getNextValue(){ + Object nextValue = super.getNextValue(); + if(nextValue == null) + return getMinimum(); + else + return nextValue; + } + + @Override + public Object getPreviousValue(){ + Object previousValue = super.getPreviousValue(); + if(previousValue == null) + return getMaximum(); + else + return previousValue; + } + + public Object getValue(){ + return super.getValue(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/ModifierEditorDialog.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,156 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Frame; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; + +/** + * A dialog showing a list of checkboxes. By selecting the checkboxes the user can choose + * which modifiers are assigned to a property value. + */ +@SuppressWarnings("serial") +public class ModifierEditorDialog extends JDialog { + + private ModifierEditorDialog(JDialog parent, List<String> modifierTypes, Set<Integer> modifierIndexes){ + super(parent, resources.getString("dialog.modifier_editor.title"), true); + init(modifierTypes, modifierIndexes); + } + + private ModifierEditorDialog(Frame parent, List<String> modifierTypes, Set<Integer> modifierIndexes){ + super(parent, resources.getString("dialog.modifier_editor.title"), true); + init(modifierTypes, modifierIndexes); + } + + private void init(List<String> modifierTypes, Set<Integer> modifierIndexes){ + listenerManager = new ListenerManager(); + createComponents(); + + panel.setLayout(new GridBagLayout()); + + checkBoxes = new JCheckBox[modifierTypes.size()]; + GridBagUtilities gridBagUtils = new GridBagUtilities(); + int i = 0; + for(String modifierType : modifierTypes){ + panel.add(new JLabel(modifierType), gridBagUtils.label()); + checkBoxes[i] = new JCheckBox(); + if(modifierIndexes.contains(i)) + checkBoxes[i].setSelected(true); + panel.add(checkBoxes[i],gridBagUtils.field()); + i++; + } + + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + okButton.addActionListener(listenerManager); + cancelButton.addActionListener(listenerManager); + panel.add(buttonPanel,gridBagUtils.all()); + + setContentPane(panel); + setResizable(false); + pack(); + } + + /** + * Shows a dialog with the checkboxes for the user to tick. + * + * @param parent the parent JDialog this dialog will appear in front of + * @param modifierTypes a list of the modifier that will be shown to the user, each near a checkbox + * @param modifiers a set of modifiers indexes. The {@code modifierTypes} at the specified indexes will + * be shown as already ticked + * + * @return a reference to {@code modifiers} after it has been updated according to the user selections. + */ + public static Set<Integer> showDialog(JDialog parent, List<String> modifierTypes, Set<Integer> modifiers){ + ModifierEditorDialog.modifiers = modifiers; + dialog = new ModifierEditorDialog(parent, modifierTypes, modifiers); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + return ModifierEditorDialog.modifiers; + + } + + /** + * Shows a dialog with the checkboxes for the user to tick. + * + * @param parent the parent Frame this dialog will appear in front of + * @param modifierTypes a list of the modifier that will be shown to the user, each near a checkbox + * @param modifiers a set of modifiers indexes. The {@code modifierTypes} at the specified indexes will + * be shown as already ticked + * + * @return a reference to {@code modifiers} after it has been updated according to the user selections. + */ + public static Set<Integer> showDialog(Frame parent, List<String> modifierTypes, Set<Integer> modifiers){ + ModifierEditorDialog.modifiers = modifiers; + dialog = new ModifierEditorDialog(parent, modifierTypes, modifiers); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + return ModifierEditorDialog.modifiers; + + } + + private void createComponents(){ + panel = new JPanel(); + buttonPanel = new JPanel(); + okButton = new JButton(resources.getString("dialog.ok_button")); + cancelButton = new JButton(resources.getString("dialog.cancel_button")); + } + + private JPanel panel; + private JPanel buttonPanel; + private JButton okButton; + private JButton cancelButton; + private JCheckBox[] checkBoxes; + private ListenerManager listenerManager; + + private static Set<Integer> modifiers; + private static ModifierEditorDialog dialog; + private static ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + + private class ListenerManager implements ActionListener { + @Override + public void actionPerformed(ActionEvent evt) { + Object source = evt.getSource(); + if(source.equals(okButton)){ + for(int i=0;i<checkBoxes.length;i++) + if(checkBoxes[i].isSelected()) + modifiers.add(i); + else + modifiers.remove(i); + dispose(); + }else if(source.equals(cancelButton)){ + dispose(); + } + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Node.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,371 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramEdge; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; + +/** + * An node in a graph. {@code Node} objects are used in a {@code GraphPanel} to render diagram nodes visually. + * {@code Node} objects are used in the tree representation of the diagram as well, as they're + * subclasses of {@link DiagramNode} + * + */ +@SuppressWarnings("serial") +public abstract class Node extends DiagramNode implements GraphElement{ + + /** + * Constructor to be called by sub classes + * + * @param type the type of the new node. All nodes with this type will be + * put under the same tree node in the tree representation + * @param properties the properties of this node + */ + public Node(String type, NodeProperties properties){ + super(type,properties); + attachedEdges = new ArrayList<Edge>(); + } + + /* --- DiagramNode abstract methods implementation --- */ + @Override + public int getEdgesNum(){ + return attachedEdges.size(); + } + + @Override + public Edge getEdgeAt(int index){ + return attachedEdges.get(index); + } + + @Override + public boolean addEdge(DiagramEdge e){ + return attachedEdges.add((Edge)e); + } + + @Override + public boolean removeEdge(DiagramEdge e){ + return attachedEdges.remove((Edge)e); + } + + @Override + public Node getExternalNode(){ + return null; + } + + @Override + public void setExternalNode(DiagramNode node){ + throw new UnsupportedOperationException(); + } + + @Override + public Node getInternalNodeAt(int i){ + throw new UnsupportedOperationException(); + } + + @Override + public int getInternalNodesNum(){ + return 0; + } + + @Override + public void addInternalNode(DiagramNode node){ + throw new UnsupportedOperationException(); + } + + @Override + public void removeInternalNode(DiagramNode node){ + throw new UnsupportedOperationException(); + } + + @Override + 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(source); + } + } + + @Override + public void translate( Point2D p , double dx, double dy, Object source){ + translateImplementation( p, dx, dy); + for(int i=0; i< getInternalNodesNum();i++){ + getInternalNodeAt(i).translate(p, dx, dy,source); + } + notifyChange(new ElementChangedEvent(this, this, "translate", source)); + } + + @Override + protected void setNotes(String notes,Object source){ + this.notes = notes; + notifyChange(new ElementChangedEvent(this,this,"notes",source)); + } + + /** + * The actual implementation of {@code translate()}. The {@code translate} method + * when called will, in turn, call this method, and then call all the registered + * change listeners in order to notify them that the node has been translated. + * + * @param p the point we are translating from + * @param dx the amount to translate in the x-direction + * @param dy the amount to translate in the y-direction + */ + protected abstract void translateImplementation(Point2D p , double dx, double dy); + + /** + * Tests whether the node contains a point. + * @param aPoint the point to test + * @return true if this node contains aPoint + */ + public abstract boolean contains(Point2D aPoint); + + @Override + public abstract Rectangle2D getBounds(); + + @Override + public void startMove(Point2D p,Object source){ + /* useless, here just to comply with the GraphElement interface */ + } + + @Override + public abstract Point2D getConnectionPoint(Direction d); + + @Override + public void draw(Graphics2D g2){ + if(!"".equals(getNotes())){ + Rectangle2D bounds = getBounds(); + Color oldColor = g2.getColor(); + g2.setColor(GraphPanel.GRABBER_COLOR); + g2.fill(new Rectangle2D.Double(bounds.getX() - MARKER_SIZE / 2, bounds.getY() - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE)); + g2.setColor(oldColor); + } + } + + /** + * Returns the geometric shape of this node + * + * @return the shape of this node + */ + public abstract Shape getShape(); + + /** + * Encodes the internal data of this node (position, name, properties, modifiers) in XML format. + * + * The saved data can be retrieved and set back via {@code decode}. + * + * @param doc An XMl document + * @param parent the parent XML tag this node tag will be nested in + */ + public void encode(Document doc, Element parent){ + parent.setAttribute(PersistenceManager.NAME,getName()); + + Element positionTag = doc.createElement(PersistenceManager.POSITION); + Rectangle2D bounds = getBounds(); + positionTag.setAttribute(PersistenceManager.X, String.valueOf(bounds.getX())); + positionTag.setAttribute(PersistenceManager.Y, String.valueOf(bounds.getY())); + parent.appendChild(positionTag); + + if(getProperties().isEmpty()) + return; + + Element propertiesTag = doc.createElement(PersistenceManager.PROPERTIES); + parent.appendChild(propertiesTag); + for(String type : getProperties().getTypes()){ + List<String> values = getProperties().getValues(type); + if(values.isEmpty()) + continue; + Element propertyTag = doc.createElement(PersistenceManager.PROPERTY); + propertiesTag.appendChild(propertyTag); + + Element typeTag = doc.createElement(PersistenceManager.TYPE); + typeTag.appendChild(doc.createTextNode(type)); + propertyTag.appendChild(typeTag); + + int index = 0; + for(String value : values){ + Element elementTag = doc.createElement(PersistenceManager.ELEMENT); + propertyTag.appendChild(elementTag); + + Element valueTag = doc.createElement(PersistenceManager.VALUE); + valueTag.appendChild(doc.createTextNode(value)); + elementTag.appendChild(valueTag); + + + Set<Integer> modifierIndexes = getProperties().getModifiers(type).getIndexes(index); + if(!modifierIndexes.isEmpty()){ + Element modifiersTag = doc.createElement(PersistenceManager.MODIFIERS); + StringBuilder builder = new StringBuilder(); + for(Integer i : modifierIndexes ) + builder.append(i).append(' '); + builder.deleteCharAt(builder.length()-1);//remove last space + modifiersTag.appendChild(doc.createTextNode(builder.toString())); + elementTag.appendChild(modifiersTag); + } + index++; + } + } + } + + /** + * Sets the internal data of this node (position, name, properties, modifiers) from an XML file + * node tag previously encoded via {@code encode} + * + * @param doc An XMl document + * @param nodeTag the XML {@code PersistenceManager.NODE } tag with data for this node + * @throws IOException if something goes wrong when reading the document. E.g. when the file is corrupted + * + * @see uk.ac.qmul.eecs.ccmi.gui.persistence + */ + public void decode(Document doc, Element nodeTag) throws IOException{ + setName(nodeTag.getAttribute(PersistenceManager.NAME),DiagramEventSource.PERS); + try{ + setId(Integer.parseInt(nodeTag.getAttribute(PersistenceManager.ID))); + }catch(NumberFormatException nfe){ + throw new IOException(); + } + + if(nodeTag.getElementsByTagName(PersistenceManager.POSITION).item(0) == null) + throw new IOException(); + Element positionTag = (Element)nodeTag.getElementsByTagName(PersistenceManager.POSITION).item(0); + double dx,dy; + try{ + dx = Double.parseDouble(positionTag.getAttribute(PersistenceManager.X)); + dy = Double.parseDouble(positionTag.getAttribute(PersistenceManager.Y)); + }catch(NumberFormatException nfe){ + throw new IOException(); + } + Rectangle2D bounds = getBounds(); + translate(new Point2D.Double(0,0), dx - bounds.getX(), dy - bounds.getY(),DiagramEventSource.PERS); + + NodeList propList = nodeTag.getElementsByTagName(PersistenceManager.PROPERTY); + NodeProperties properties = getProperties(); + for(int j=0; j<propList.getLength();j++){ + Element propertyTag = (Element)propList.item(j); + + if(propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null) + throw new IOException(); + Element pTypeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0); + String propertyType = pTypeTag.getTextContent(); + + /* scan all the <Element> of the current <Property>*/ + NodeList elemValueList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT); + for(int h=0; h<elemValueList.getLength(); h++){ + + Element elemTag = (Element)elemValueList.item(h); + /* get the <value> */ + if(elemTag.getElementsByTagName(PersistenceManager.VALUE).item(0) == null) + throw new IOException(); + Element valueTag = (Element)elemTag.getElementsByTagName(PersistenceManager.VALUE).item(0); + String value = valueTag.getTextContent(); + + /* <modifiers>. need to go back on the prototypes because the content of <modifier> is a list */ + /* of int index pointing to the modifiers type, defined just in the prototypes */ + Element prototypesTag = (Element)doc.getElementsByTagName(PersistenceManager.PROTOTYPES).item(0); + Modifiers modifiers = null; + try { + modifiers = properties.getModifiers(propertyType); + }catch(IllegalArgumentException iae){ + throw new IOException(iae); + } + if(!modifiers.isNull()){ + Element modifiersTag = (Element)((Element)elemValueList.item(h)).getElementsByTagName(PersistenceManager.MODIFIERS).item(0); + if(modifiersTag != null){ //else there are no modifiers specified for this property value + Set<Integer> indexesToAdd = new LinkedHashSet<Integer>(); + String indexesString = modifiersTag.getTextContent(); + String[] indexes = indexesString.split(" "); + for(String s : indexes){ + try{ + int index = Integer.parseInt(s); + NodeList templatePropList = prototypesTag.getElementsByTagName(PersistenceManager.PROPERTY); + String modifiersType = null; + /* look at the property prototypes to see which modifier the index is referring to. * + * The index is in fact the id attribute of the <Modifier> tag in the prototypes section */ + for(int k=0; k<templatePropList.getLength();k++){ + Element prototypePropTag = (Element)templatePropList.item(k); + Element prototypePropTypeTag = (Element)prototypePropTag.getElementsByTagName(PersistenceManager.TYPE).item(0); + + if(propertyType.equals(prototypePropTypeTag.getTextContent())){ + NodeList proptotypeModifierList = prototypePropTag.getElementsByTagName(PersistenceManager.MODIFIER); + for(int m = 0 ; m<proptotypeModifierList.getLength();m++){ + if(index == Integer.parseInt(((Element)proptotypeModifierList.item(m)).getAttribute(PersistenceManager.ID))){ + Element modifierTypeTag = (Element)((Element)proptotypeModifierList.item(m)).getElementsByTagName(PersistenceManager.TYPE).item(0); + modifiersType = modifierTypeTag.getTextContent(); + } + } + } + } + if(modifiersType == null) // the index must point to a valid modifier's id + throw new IOException(); + indexesToAdd.add(Integer.valueOf(modifiers.getTypes().indexOf(modifiersType))); + }catch(NumberFormatException nfe){ + throw new IOException(nfe); + } + } + addProperty(propertyType, value,DiagramEventSource.PERS);//whether propertyType actually exist in the prototypes has been already checked + setModifierIndexes(propertyType, h, indexesToAdd,DiagramEventSource.PERS); + }else + addProperty(propertyType, value,DiagramEventSource.PERS); + }else + addProperty(propertyType, value,DiagramEventSource.PERS); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public Object clone(){ + Node clone = (Node)super.clone(); + clone.attachedEdges = (ArrayList<Edge>) attachedEdges.clone(); + return clone; + } + + /** + * An array of references to the edges attached to this node + */ + protected ArrayList<Edge> attachedEdges; + + private final int MARKER_SIZE = 7; + /** + * The shadow color of nodes + */ + protected static final Color SHADOW_COLOR = Color.LIGHT_GRAY; + public static final int SHADOW_GAP = 2; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/PropertyEditorDialog.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,206 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTable; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; + +/** + * A Dialog for editing the {@link NodeProperties} of a diagram node. + * + */ +@SuppressWarnings("serial") +public class PropertyEditorDialog extends JDialog { + + private PropertyEditorDialog(Frame parent){ + super(parent, resources.getString("dialog.property_editor.title") , true); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + + addWindowListener(new WindowAdapter(){ + @Override + public void windowClosing(WindowEvent event){ + result = null; + dispose(); + } + }); + + listenerManager = new ListenerManager(); + + createComponents(); + setContentPane(scrollPane); + + panel.setLayout(new GridBagLayout()); + + GridBagUtilities gridBagUtils = new GridBagUtilities(); + panel.add(topSeparator,gridBagUtils.all()); + + int i = 0; + for(String type : properties.getTypes()){ + PropertyPanel propertyPanel = new PropertyPanel(type, properties.getValues(type), properties.getModifiers(type)); + panel.add(propertyPanel,gridBagUtils.all()); + propertyPanels[i++] = propertyPanel; + } + + if(!properties.getTypes().isEmpty()) + panel.add(bottomSeparator, gridBagUtils.all()); + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + panel.add(buttonPanel, gridBagUtils.all()); + + okButton.addActionListener(listenerManager); + cancelButton.addActionListener(listenerManager); + pack(); + } + + /** + * A static method to show a {@code PropertyEditorDialog}. Via this dialog a user can + * specify the entries of a {@code NodeProperties} object and their modifiers, if any have been + * defined during the template creation. + * + * @param parent The parent {@code Frame} of the dialog + * @param properties an instance of {@code NodeProeprties} whose value will be shown in the dialog + * for the user to edit + * @return a reference to {@code properties} containing the new values entered by the user or + * {@code null} if the user presses the cancel button or closes the window. + */ + public static NodeProperties showDialog(Frame parent, NodeProperties properties){ + if(properties == null) + throw new IllegalArgumentException(resources.getString("dialog.property_editor.error.property_null")); + PropertyEditorDialog.properties = properties; + dialog = new PropertyEditorDialog(parent); + dialog.setLocationRelativeTo(parent); + dialog.setVisible(true); + return result; + } + + private void createComponents(){ + panel = new JPanel(); + buttonPanel = new JPanel(); + propertyPanels = new PropertyPanel[PropertyEditorDialog.properties.getTypes().size()]; + scrollPane = new JScrollPane( + panel, + JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER + ); + bottomSeparator = new JSeparator(); + topSeparator = new JSeparator(); + okButton = new JButton(resources.getString("dialog.ok_button")); + cancelButton = new JButton(resources.getString("dialog.cancel_button")); + } + + private JPanel panel; + private JPanel buttonPanel; + private PropertyPanel[] propertyPanels; + private JScrollPane scrollPane; + private JSeparator topSeparator; + private JSeparator bottomSeparator; + private JButton okButton; + private JButton cancelButton; + private ListenerManager listenerManager; + + private static PropertyEditorDialog dialog; + private static NodeProperties properties; + private static NodeProperties result; + private static ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + + private class PropertyPanel extends JPanel{ + public PropertyPanel(String propertyType, List<String> values, final Modifiers modifiers ){ + setLayout(new BoxLayout(this,BoxLayout.Y_AXIS)); + model = new PropertyTableModel(propertyType, values, modifiers); + if(modifiers != null) + for(int i=0; i< values.size(); i++){ + model.setIndexesAt(i, modifiers.getIndexes(i)); + } + table = new JTable(model); + table.setPreferredScrollableViewportSize(new Dimension(250, 70)); + table.setFillsViewportHeight(true); + + add(new JScrollPane(table)); + /* we can edit modifiers only if one or more modifier types have been defined for this property type */ + if(!modifiers.isNull()){ + editModifiers = new JButton(resources.getString("dialog.property_editor.edit_modifiers_button")); + editModifiers.setAlignmentX(RIGHT_ALIGNMENT); + add(editModifiers); + editModifiers.addActionListener(new ActionListener(){ + @Override + public void actionPerformed(ActionEvent arg0) { + int row = table.getSelectedRow(); + if((row == -1)||(row == model.getRowCount()-1)) + return; + Set<Integer> indexes; + + indexes = ModifierEditorDialog.showDialog(PropertyEditorDialog.this, modifiers.getTypes(), model.getIndexesAt(row)); + model.setIndexesAt(row, indexes); + } + }); + } + add(Box.createRigidArea(new Dimension(0,5))); + } + + JTable table; + PropertyTableModel model; + JButton editModifiers; + } + + private class ListenerManager implements ActionListener{ + @Override + public void actionPerformed(ActionEvent evt) { + Object source = evt.getSource(); + if(source.equals(okButton)){ + for(int i=0; i<propertyPanels.length;i++){ + PropertyTableModel model = propertyPanels[i].model; + properties.clear(model.getColumnName(0)); + for(int j=0; j< model.getRowCount();j++){ + String value = model.getValueAt(j, 0).toString().trim(); + if(!value.equals("")){ + properties.addValue(model.getColumnName(0),value , model.getIndexesAt(j)); + } + } + } + result = properties; + dispose(); + }else{ + result = null; + dispose(); + } + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/PropertyTableModel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,169 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui; + +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import java.util.Set; + +import javax.swing.table.AbstractTableModel; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; + + +/** + * A table model containing the property values currently edited in a table of a {@code PropertyEditorDialog} + * and the modifiers assigned to them + * in the form of an array of indexes pointing to the modifiers types + */ +@SuppressWarnings("serial") +public class PropertyTableModel extends AbstractTableModel { + + /** + * Construct a new {@code PropertyTableModel}. + * + * @param propertyType the type of the node property related to this table model + * @param values the list of values for this node property + * @param modifiers the modifiers for this node property + */ + public PropertyTableModel(String propertyType, List<String> values, Modifiers modifiers ){ + data = new ArrayList<ModifierString>(); + for(int i = 0; i< values.size();i++){ + String value = values.get(i); + data.add(new ModifierString(value, (modifiers == null) ? null : modifiers.getIndexes(i))); + } + data.add(new ModifierString(null)); + columnName = propertyType; + } + + @Override + public int getColumnCount() { + return 1; + } + + @Override + public int getRowCount() { + return data.size(); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return data.get(rowIndex); + } + + @Override + public String getColumnName(int column){ + return columnName; + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex){ + /* we filled up the last row, create another one */ + if((rowIndex == data.size()-1)&&(!value.toString().equals(""))){ + data.add(new ModifierString(null)); + data.get(rowIndex).value = value.toString(); + fireTableRowsInserted(data.size()-1, data.size()-1); + fireTableCellUpdated(rowIndex,columnIndex); + }else if((rowIndex != data.size()-1)&&(value.toString().equals(""))){ + data.remove(rowIndex); + fireTableRowsDeleted(rowIndex,rowIndex); + }else { + data.get(rowIndex).value = value.toString(); + fireTableCellUpdated(rowIndex,columnIndex); + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex){ + return true; + } + + /** + * Returns the indexes (pointing to the modifiers types) of the property value at the specified row in the table + * with this model. + * + * @param row the row of the property value + * @return the modifiers type indexes for the specified property value + */ + public Set<Integer> getIndexesAt(int row){ + return data.get(row).modifierIndexes; + } + + /** + * Set the the indexes (pointing to the modifiers types) of the property value at the specified row in the table + * with this model. + * + * @param row he row of the property value + * @param indexes the modifiers type indexes for the specified property value + */ + public void setIndexesAt(int row, Set<Integer> indexes){ + data.get(row).modifierIndexes = new HashSet<Integer>(); + data.get(row).modifierIndexes.addAll(indexes); + } + + /** + * Set the the indexes (pointing to the modifiers types) of the property value at the specified row in the table + * with this model. + * + * @param row he row of the property value + * @param indexes the modifiers type indexes for the specified property value + */ + public void setIndexesAt(int row, Integer[] indexes){ + data.get(row).modifierIndexes = new HashSet<Integer>(); + for(int i=0; i<indexes.length; i++) + data.get(row).modifierIndexes.add(indexes[i]); + } + + private List<ModifierString> data; + private String columnName; + + private class ModifierString { + ModifierString(String value, Set<Integer> s){ + this.value = value; + modifierIndexes = new HashSet<Integer>(); + if(s != null){ + modifierIndexes.addAll(s); + } + } + + ModifierString(Set<Integer> s){ + this("",s); + } + + @Override + public boolean equals(Object o){ + return value.equals(o); + } + + @Override + public int hashCode(){ + return value.hashCode(); + } + + @Override + public String toString(){ + return value; + } + + private String value; + private Set<Integer> modifierIndexes; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/ResourceFactory.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,112 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.event.ActionListener; +import java.beans.EventHandler; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.KeyStroke; + +/** + * A factory class for swing components creation support. + * + * Components that are created via this class are, in turn created by {@code SpeechMenuFactory} methods. + * This class handles the labelling and other menu properties (such as accelerators) + * using the {@code ResourceBundle} passed as argument to the constructor. + */ +class ResourceFactory{ + public ResourceFactory(ResourceBundle bundle){ + this.bundle = bundle; + } + + public JMenu createMenu(String prefix){ + String text = bundle.getString(prefix + ".text"); + JMenu menu = SpeechMenuFactory.getMenu(text); + try{ + String mnemonic = bundle.getString(prefix + ".mnemonic"); + menu.setMnemonic(mnemonic.charAt(0)); + }catch (MissingResourceException exception){ + // ok not to set mnemonic + } + + try{ + String tooltip = bundle.getString(prefix + ".tooltip"); + menu.setToolTipText(tooltip); + } + catch (MissingResourceException exception){ + // ok not to set tooltip + } + return menu; + } + + public JMenuItem createMenuItem(String prefix, + Object target, String methodName){ + return createMenuItem(prefix, + (ActionListener) EventHandler.create( + ActionListener.class, target, methodName)); + } + + public JMenuItem createMenuItem(String prefix, + ActionListener listener){ + String text = bundle.getString(prefix + ".text"); + JMenuItem menuItem = SpeechMenuFactory.getMenuItem(text); + return configure(menuItem, prefix, listener); + } + + public JMenuItem createCheckBoxMenuItem(String prefix, + ActionListener listener){ + String text = bundle.getString(prefix + ".text"); + JMenuItem menuItem = SpeechMenuFactory.getJCheckBoxMenuItem(text); + return configure(menuItem, prefix, listener); + } + + private JMenuItem configure(JMenuItem menuItem, + String prefix, ActionListener listener){ + menuItem.addActionListener(listener); + try{ + String mnemonic = bundle.getString(prefix + ".mnemonic"); + menuItem.setMnemonic(mnemonic.charAt(0)); + }catch (MissingResourceException exception){ + // ok not to set mnemonic + } + + try{ + String accelerator = bundle.getString(prefix + ".accelerator"); + menuItem.setAccelerator(KeyStroke.getKeyStroke(accelerator)); + }catch (MissingResourceException exception){ + // ok not to set accelerator + } + + try{ + String tooltip = bundle.getString(prefix + ".tooltip"); + menuItem.setToolTipText(tooltip); + }catch (MissingResourceException exception){ + // ok not to set tooltip + } + return menuItem; + } + + private ResourceBundle bundle; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechMenuFactory.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,229 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.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; +import javax.swing.ComponentInputMap; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComponent; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.KeyStroke; +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.speech.NarratorFactory; + +/* + * This class provides a version of JMenuItem, JCheckBox, and JMenu which emit an "error" sound when + * they're disabled and the user tries to use it by an accelerator + */ +@SuppressWarnings("serial") +class SpeechMenuFactory extends JMenu { + + /* implements the singleton pattern and keeps a static reference to the menuBar used by JMenuItems and JMenus */ + public static JMenuBar getMenuBar(){ + if(menuBar == null){ + menuBar = new SpeechMenuBar(); + } + return menuBar; + } + + public static JMenuItem getMenuItem(String text){ + return new SpeechMenuItem(text); + } + + public static JMenu getMenu(String text){ + return new JMenu(text){ + @Override + public void menuSelectionChanged(boolean isIncluded){ + super.menuSelectionChanged(isIncluded); + if(isIncluded && !wasMouse){ + String menuType = resources.getString("menufactory.menu"); + if(getMenuBar().getComponentIndex(this) == -1){ + menuType = resources.getString("menufactory.submenu"); + }; + NarratorFactory.getInstance().speak(getAccessibleContext().getAccessibleName()+ " "+ menuType); + } + wasMouse = false; + } + + @Override + public void processMouseEvent(MouseEvent e){ + wasMouse = true; + super.processMouseEvent(e); + } + + private boolean wasMouse = false; + }; + } + + public static JCheckBoxMenuItem getJCheckBoxMenuItem(String text){ + return new 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) { + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + }; + + private static class SpeechMenuBar extends JMenuBar{ + SpeechMenuBar() { + setInputMap(SpeechMenuBar.WHEN_IN_FOCUSED_WINDOW, new ComponentInputMap(this){ + @Override + public Object get(KeyStroke keyStroke){ + if(keyStroke == null) + return null; + return super.get(keyStroke); + } + }); + } + + @Override + public void processKeyEvent(KeyEvent e, MenuElement[] path, MenuSelectionManager manager) { + super.processKeyEvent(e,path,manager); + if(e.getKeyCode() == KeyEvent.VK_ESCAPE){ + NarratorFactory.getInstance().speak(resources.getString("menufactory.leaving")); + } + } + } + + /* + * this class implements a menu item which speaks out its label when + * selected with the keyboard (mouse hover will have no effect) + * it needs a reference to the menu bar it's in to implement a respond, with the error sound, to + * the accelerator even when it's disabled (normally no listeners are called otherwise) + * + */ + private static class SpeechMenuItem extends JMenuItem{ + @Override + public void processMouseEvent(MouseEvent e ){ + wasMouse = true; + super.processMouseEvent(e); + } + + public SpeechMenuItem(String text){ + super(text); + /* bind ACCELERATOR with the error action */ + getMenuBar().getActionMap().put(ACCELERATOR, errorAction); + wasMouse = false; + if(text.trim().endsWith("...")){ + /* replace the ... in the accessible name with the voice version of it */ + String accName = getAccessibleContext().getAccessibleName().replaceAll("...\\s*$", ""); + getAccessibleContext().setAccessibleName(accName +" "+ resources.getString("menufactory.3dot")); + } + } + + @Override + public void menuSelectionChanged(boolean isIncluded){ + super.menuSelectionChanged(isIncluded); + if(isIncluded && !wasMouse){ + String disabled = isEnabled() ? "" : resources.getString("menufactory.disabled"); + String accelerator = ""; + if(getAccelerator() != null){ + accelerator = resources.getString("menufactory.ctrl")+" "+ getAccelerator().toString().substring(getAccelerator().toString().lastIndexOf(' ')); + } + NarratorFactory.getInstance().speak(getAccessibleContext().getAccessibleName()+disabled+accelerator); + } + wasMouse = false; + } + + @Override + public void setEnabled(boolean b){ + super.setEnabled(b); + if(getAccelerator() == null) + return; + if(b == false){ + /* if the menu item gets disabled, then set up an action in the menuBar to respond to + * the accelerator key stroke, so that the user gets a feedback (error sound) + * even though the listeners don't get called (as the menu item is disabled) + */ + getMenuBar().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(getAccelerator(), ACCELERATOR); + }else{ + getMenuBar().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(getAccelerator(), "none"); + } + } + + private boolean wasMouse; + }; + + private static 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()); + private static final String ACCELERATOR = "accelerator"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechOptionPane.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,613 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Frame; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.text.MessageFormat; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SwingWorker; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.text.JTextComponent; + +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.Narrator; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +/** + * + * 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 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 message 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); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + 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,(JButton)evt.getSource()); + } + }; + okButton.addActionListener(buttonListener); + cancelButton.addActionListener(buttonListener); + + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + 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 the title of this option pane + */ + 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 presses 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 source the button that triggered the closing of {@code dialog} + */ + protected void onClose(JDialog dialog, JButton source){ + optPane.setValue(source); + dialog.dispose(); + } + + private String title; + private JOptionPane optPane; + private JButton okButton; + private JButton cancelButton; + + + /* -------- STATIC METHODS ----------- */ + + /** + * Shows a dialog with a text area requesting input for the user. + * + * @param parentComponent the parent {@code Component} for the dialog + * @param message a displayed in the dialog, such text is also uttered by the {@code Narrator} + * @param text the initial text the text area contains when the dialog is displayed + * + * @return the new text entered by the user + */ + public static String showTextAreaDialog(Component parentComponent, String message, String text){ + JTextArea textArea = new JTextArea(NOTES_TEXT_AREA_ROW_SIZE,NOTES_TEXT_AREA_COL_SIZE); + textArea.setText(text); + NarratorFactory.getInstance().speak(message); + return textComponentDialog(parentComponent, message, textArea); + } + + /** + * Shows a dialog with a text field requesting input from the user + * + * @param parentComponent the parent {@code Component} for the dialog + * @param message a message displayed in the dialog, such text is also uttered by the {@code Narrator} + * @param initialSelectionValue the initial text the text field contains when the dialog is displayed + * + * @return the text entered by the user + */ + public static String showInputDialog(Component parentComponent, String message, String initialSelectionValue){ + final JTextField textField = new JTextField(initialSelectionValue); + textField.selectAll(); + NarratorFactory.getInstance().speak(message); + return textComponentDialog(parentComponent, message, textField); + } + + private static String textComponentDialog(Component parentComponent, String message, final JTextComponent textComponent){ + Object componentToDisplay = textComponent; + if(textComponent instanceof JTextArea) + componentToDisplay = new JScrollPane(textComponent); + + Object[] displayObjects = { new JLabel(message), componentToDisplay }; + final JOptionPane optPane = new JOptionPane(); + optPane.setMessage(displayObjects); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(OK_CANCEL_OPTION); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + final JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.input")); + if(textComponent instanceof JTextArea) + dialog.setResizable(true); + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + textComponent.requestFocusInWindow(); + } + }); + + SpeechUtilities.changeTabListener(optPane,dialog); + textComponent.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + textComponent.setEditable(true); + // start the editing sound + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + dialog.dispose(); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + + if(optPane.getValue() == null)//window closed + return null; + else if(((Integer)optPane.getValue()).intValue() == CANCEL_OPTION || ((Integer)optPane.getValue()).intValue() == CLOSED_OPTION)//pressed on cancel + return null; + else{ // pressed on OK + return textComponent.getText().trim(); + } + } + + /** + * Shows a dialog with a {@code JComboBox} requesting selection from the user + * + * @param parentComponent the parent {@code Component} for the dialog + * @param message a message displayed in the dialog, such text is also uttered by the {@code Narrator} + * @param options options for the {@code JComboBox} + * @param initialValue the options value selected when the dialog is shown + * @return the option selected by the user + */ + public static Object showSelectionDialog(Component parentComponent, String message, Object[] options, Object initialValue){ + final LoopComboBox comboBox = new LoopComboBox(options); + comboBox.setSelectedItem(initialValue); + Object[] displayObjects = { new JLabel(message), comboBox }; + JOptionPane optPane = new JOptionPane(); + optPane.setMessage(displayObjects); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(OK_CANCEL_OPTION); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + NarratorFactory.getInstance().speak(message+", "+SpeechUtilities.getComponentSpeech(comboBox)); + final JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.select")); + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + comboBox.requestFocusInWindow(); + } + }); + + comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + + SpeechUtilities.changeTabListener(optPane,dialog); + // start the editing sound + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + dialog.dispose(); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + if(optPane.getValue() == null)//window closed + return null; + else if(((Integer)optPane.getValue()).intValue() == CANCEL_OPTION || ((Integer)optPane.getValue()).intValue() == CLOSED_OPTION)//pressed on cancel )//pressed on cancel + return null; + else{ // pressed on OK + return comboBox.getSelectedItem(); + } + } + + /** + * Shows the dialog with a {@code JSpinner} requesting the selection of the speech rate + * of the main voice of the {@code Narrator} + * + * @param parentComponent the parent {@code Component} for the dialog + * @param message a message displayed in the dialog, such text is also uttered by the {@code Narrator} + * @param value the initial value + * @param min the minimum value of the spinner + * @param max the maximum value of the spinner + * @return the selected integer value or {@code null} if the user cancels the dialog + */ + public static Integer showNarratorRateDialog(Component parentComponent, String message, int value, int min, int max){ + NarratorFactory.getInstance().speak(message); + final JSpinner spinner = new JSpinner(new LoopSpinnerNumberModel(value,min,max)); + JFormattedTextField tf = ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField(); + tf.setEditable(false); + tf.setFocusable(false); + tf.setBackground(Color.white); + + Object[] displayObjects = { new JLabel(message), spinner}; + final JOptionPane optPane = new JOptionPane(); + optPane.setMessage(displayObjects); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(OK_CANCEL_OPTION); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + final JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.input")); + SpeechUtilities.changeTabListener(optPane,dialog); + + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + spinner.requestFocusInWindow(); + } + }); + spinner.addChangeListener(new ChangeListener(){ + @Override + public void stateChanged(ChangeEvent evt) { + JSpinner s = (JSpinner)(evt.getSource()); + NarratorFactory.getInstance().setRate((Integer)s.getValue()); + NarratorFactory.getInstance().speak(s.getValue().toString()); + } + }); + // start the editing sound + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + dialog.dispose(); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + + /* set the speech rate back to the value passed as argument */ + NarratorFactory.getInstance().setRate(value); + if(optPane.getValue() == null)//window closed + return null; + else if(((Integer)optPane.getValue()).intValue() == CANCEL_OPTION || ((Integer)optPane.getValue()).intValue() == CLOSED_OPTION)//pressed on cancel + return null; + else{ // pressed on OK + return (Integer)spinner.getValue(); + } + } + + /** + * Brings up a dialog with selected options requesting user to confirmation. + * + * @param parentComponent the parent {@code Component} for the dialog + * @param message a message displayed in the dialog, such text is also uttered by the {@code Narrator} + * @param optionType an integer designating the options available on the dialog + * @return an integer indicating the option selected by the user + */ + public static int showConfirmDialog(Component parentComponent, String message, int optionType){ + NarratorFactory.getInstance().speak(message); + JOptionPane optPane = new JOptionPane(); + optPane.setMessage(message); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(optionType); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.confirm")); + SpeechUtilities.changeTabListener(optPane,dialog); + + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + dialog.dispose(); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + + if(optPane.getValue() == null)//window closed + return CANCEL_OPTION; + else + return ((Integer)optPane.getValue()).intValue(); + } + + /** + * Displays a message to the user. + * + * @param parentComponent the parent {@code Component} for the dialog + * @param message the message displayed in the dialog, such text is also uttered by the {@code Narrator} + * @param messageType the type of message to be displayed + */ + public static void showMessageDialog(Component parentComponent, String message, int messageType){ + NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("dialog.speech_option_pane.message"), message)); + JOptionPane optPane = new JOptionPane(); + optPane.setMessage(message); + optPane.setMessageType(messageType); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.confirm")); + SpeechUtilities.changeTabListener(optPane,dialog); + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + dialog.dispose(); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + } + + /** + * Displays an error message to the user. + * + * @param parentComponent the parent {@code Component} for the dialog + * @param message the message displayed in the dialog, such text is also uttered by the {@code Narrator} + */ + public static void showMessageDialog(Component parentComponent, String message){ + showMessageDialog(parentComponent,message,ERROR_MESSAGE); + } + + /** + * Execute a ProgressDialogWorker task and + * shows an indeterminate progress bar dialog if the task is not completed after + * {@code millisToDecideToPopup}. The user can use the dialog <i>cancel</i> button + * to cancel the task. + * + * @param <T> the result type returned by the worker + * @param <V> the intermediate result type of the worker + * @param parentComponent the parent {@code Component} for the dialog + * @param message message a message displayed in the dialog, such text is also uttered by the {@code Narrator} + * @param worker a {@code ProgressDialogWorker} that is executed when this method is called + * @param millisToDecideToPopup the millisecond to let to the worker before popping the dialog up + * @return an integer indicating whether the task was completed or it was interrupted by the user + */ + public static <T,V> int showProgressDialog(Component parentComponent, String message,final ProgressDialogWorker<T,V> worker, int millisToDecideToPopup){ + JProgressBar progressBar = worker.bar; + Object displayObjects[] = {message, progressBar}; + final JOptionPane optPane = new JOptionPane(displayObjects); + optPane.setOptionType(DEFAULT_OPTION); + optPane.setOptions(new Object[] {PROGRESS_DIALOG_CANCEL_OPTION}); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + final JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.download")); + SpeechUtilities.changeTabListener(optPane,dialog); + + worker.setDialog(dialog); + worker.execute(); + try { + Thread.sleep(millisToDecideToPopup); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); //should never happen + } + if(worker.isDone()) + return OK_OPTION; + + NarratorFactory.getInstance().speak(message); + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + if( optPane.getValue() == null || + optPane.getValue() == PROGRESS_DIALOG_CANCEL_OPTION || + Integer.valueOf(CLOSED_OPTION).equals(optPane.getValue())){ + worker.cancel(true); + return CANCEL_OPTION; + } + return OK_OPTION; + } + + /** + * Shows a check box dialog to select the modifiers of a given property + * + * @param parentComponent the parent {@code Component} for the dialog + * @param message message a message displayed in the dialog, such text is also uttered by the {@code Narrator} + * @param modifierTypes the different types of modifiers that are available for a given property + * @param modifierIndexes the initial selection of modifiers as the dialog is shown + * @return a new set with the modifier indexes selected by the user + */ + public static Set<Integer> showModifiersDialog(Component parentComponent, String message, List<String> modifierTypes, Set<Integer> modifierIndexes){ + JOptionPane optPane = new JOptionPane(); + + JPanel checkBoxPanel = new JPanel(new GridLayout(0, 1)); + final JCheckBox[] checkBoxes = new JCheckBox[modifierTypes.size()]; + for(int i=0;i<checkBoxes.length;i++){ + checkBoxes[i] = new JCheckBox(modifierTypes.get(i)); + if(modifierIndexes.contains(i)) + checkBoxes[i].setSelected(true); + checkBoxPanel.add(checkBoxes[i]); + checkBoxes[i].addItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + } + NarratorFactory.getInstance().speak(message+" "+SpeechUtilities.getComponentSpeech(checkBoxes[0])); + + Object[] displayObjects = {new JLabel(message),checkBoxPanel}; + optPane.setMessage(displayObjects); + optPane.setMessageType(QUESTION_MESSAGE); + optPane.setOptionType(OK_CANCEL_OPTION); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + JDialog dialog = optPane.createDialog(parentComponent, resources.getString("dialog.speech_option_pane.modifiers")); + SpeechUtilities.changeTabListener(optPane,dialog); + + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + checkBoxes[0].requestFocusInWindow(); + } + }); + + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + dialog.dispose(); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + + if(optPane.getValue() == null)//window closed + return null; + else if(((Integer)optPane.getValue()).intValue() == CANCEL_OPTION || ((Integer)optPane.getValue()).intValue() == CLOSED_OPTION)//pressed on cancel + return null; + else{ // pressed on OK + Set<Integer> returnSet = new LinkedHashSet<Integer>(); + for(int i=0;i<checkBoxes.length;i++) + if(checkBoxes[i].isSelected()) + returnSet.add(i); + return returnSet; + } + } + + /** + * Returns the specified component's {code Frame}. + * + * @param parentComponent the component for this dialog + * @return the {@code Frame} that contains the component + */ + public static Frame getFrameForComponent(Component parentComponent){ + return JOptionPane.getFrameForComponent(parentComponent); + } + + private static ResourceBundle resources = ResourceBundle.getBundle(EditorFrame.class.getName()); + private static final int NOTES_TEXT_AREA_COL_SIZE = 10; + private static final int NOTES_TEXT_AREA_ROW_SIZE = 10; + private static final String PROGRESS_DIALOG_CANCEL_OPTION = resources.getString("dialog.speech_option_pane.cancel"); + + + public static final int QUESTION_MESSAGE = JOptionPane.QUESTION_MESSAGE; + public static final int ERROR_MESSAGE = JOptionPane.ERROR_MESSAGE; + public static final int INFORMATION_MESSAGE = JOptionPane.INFORMATION_MESSAGE; + public static final int WARNING_MESSAGE = JOptionPane.WARNING_MESSAGE; + public static final int OK_CANCEL_OPTION = JOptionPane.OK_CANCEL_OPTION; + public static final int CANCEL_OPTION = JOptionPane.CANCEL_OPTION; + public static final int OK_OPTION = JOptionPane.OK_OPTION; + public static final int CLOSED_OPTION = JOptionPane.CLOSED_OPTION; + public static final int DEFAULT_OPTION = JOptionPane.DEFAULT_OPTION; + public static final int YES_NO_OPTION = JOptionPane.YES_NO_OPTION; + public static final int YES_OPTION = JOptionPane.YES_OPTION; + public static final int NO_OPTION = JOptionPane.NO_OPTION; + + /** + * A swing worker to be passed as argument to {@code showProgressDialog}. The {@code execute} + * method can be interrupted by the user by clicking on the {@code cancel} button of the dialog. + * + * @param <T> the result type returned by this {@code SwingWorker}'s {@code doInBackground} and {@code get} methods + * @param <V> the type used for carrying out intermediate results by this {@code SwingWorker}'s {@code publish} and {@code process} methods + */ + public static abstract class ProgressDialogWorker<T,V> extends SwingWorker<T,V> { + /** + * Creates a new ProgressDialogWorker + */ + public ProgressDialogWorker(){ + bar = new JProgressBar(); + bar.setIndeterminate(true); + } + + private void setDialog(JDialog dialog){ + this.dialog = dialog; + } + + @Override + protected void done() { //executed in EDT when the work is done + if(dialog != null) + dialog.dispose(); + } + + private JDialog dialog; + private JProgressBar bar; + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/SpeechSummaryPane.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,127 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.KeyStroke; + +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +/** + * Abstract class with an one-line call to display a summary dialog. + * The summary text as well as focused components are spoken out through text to speech + * synthesis performed by the {@code Narrator} instance. + * A summary dialog has non editable text field and a button for confirmation only. + * + * + */ +public abstract class SpeechSummaryPane { + + /** + * shows the summary dialog + * @param parentComponent determines the {@code Frame} in which the dialog is displayed + * @param title the title of the displayed dialog + * @param text the text to be displayed in this dialog. his text, together with the title + * is uttered through the {@code Narrator} as soon as the dialog is shown + * @param optionType an integer designating the options available on the dialog + * either {@code OK_CANCEL_OPTION} or {@code OK_OPTION} + * @param options an array of strings indicating the possible choices the user can make + * @return an integer indicating the option selected by the user. Either {@code OK} or {@code CANCEL} + */ + public static int showDialog(Component parentComponent, String title, String text, int optionType, String[] options){ + if(optionType == OK_CANCEL_OPTION && options.length < 2) + throw new IllegalArgumentException("option type and opions number must be consistent"); + final JTextArea textArea = new JTextArea(); + textArea.setText(text); + NarratorFactory.getInstance().speak(title+". "+ text); + + JScrollPane componentToDisplay = new JScrollPane(textArea); + /* set the maximum size: if there is a lot of content yet it doesn't take the whole screen */ + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + + int editorWidth = (int)screenSize.getWidth() * 5 / 8; + int editorHeight = (int)screenSize.getHeight() * 5 / 8; + + Dimension currentSize = componentToDisplay.getPreferredSize(); + componentToDisplay.setPreferredSize(new Dimension( + Math.min(currentSize.width, editorWidth) , Math.min(currentSize.height, editorHeight))); + + Object[] displayObjects = { new JLabel(title), componentToDisplay }; + final JOptionPane optPane = new JOptionPane(); + optPane.setMessage(displayObjects); + optPane.setMessageType(JOptionPane.PLAIN_MESSAGE); + optPane.setOptionType(optionType); + /* set the options according to the option type */ + optPane.setOptions(options); + /* ctrl key will hush the TTS */ + optPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTROL,InputEvent.CTRL_DOWN_MASK),"shut_up"); + optPane.getActionMap().put("shut_up", SpeechUtilities.getShutUpAction()); + + final JDialog dialog = optPane.createDialog(parentComponent, ""); + dialog.setResizable(true); + + dialog.addWindowFocusListener(new WindowAdapter(){ + @Override + public void windowGainedFocus(WindowEvent e) { + textArea.requestFocusInWindow(); + } + }); + + SpeechUtilities.changeTabListener(optPane,dialog); + /* the textArea is not editable, so tab key event must not be consumed so that it can be picked up by the focus manager */ + textArea.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,0), "none"); + textArea.addKeyListener(SpeechUtilities.getSpeechKeyListener(false)); + textArea.setEditable(false); + // start the editing sound + SoundFactory.getInstance().startLoop(SoundEvent.EDITING); + dialog.setVisible(true); + dialog.dispose(); + SoundFactory.getInstance().stopLoop(SoundEvent.EDITING); + NarratorFactory.getInstance().shutUp(); + + if(optPane.getValue() == null)//window closed + return CANCEL; + else if(optPane.getValue().equals(options[OK]))// pressed on OK + return OK; + else //pressed on cancel + return CANCEL; + } + + public static final int OK = 0; + public static final int CANCEL = 1; + public static final int OK_CANCEL_OPTION = JOptionPane.OK_CANCEL_OPTION; + public static final int OK_OPTION = JOptionPane.OK_OPTION; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/TemplateEditor.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,78 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui; + +import java.awt.Frame; +import java.util.Collection; + +/** + * 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 <i>Swing</i> components + * to prompt the user with choices about the diagram to be created. The diagram created by + * a template editor is precisely a prototype. + * Such prototypes diagrams will then be used + * to create new diagram instances (the actual diagrams operated by the user) through clonation. + * When a diagram prototype is created, it's added in the File->new Diagram menu. + */ +public interface TemplateEditor { + + /** + * Creates a new {@code Diagram} + * + * @param frame the frame where the template editor is run + * @param existingTemplates the names of already existing templates. The creation + * of a new {@code Diagram} with an already existing name must be prevented in order + * to keep the consistency of the diagram templates. + * + * @return a new {@code Diagram} prototype + */ + public Diagram createNew(Frame frame, Collection<String> existingTemplates); + + /** + * Edits an existing {@code Diagram} prototype. + * + * @param frame the frame where the template editor is run + * @param existingTemplates the names of already existing templates. The creation + * of a new {@code Diagram} with an already existing name must be prevented in order + * to keep the consistency of the diagram templates. + * @param diagram the diagram to edit + * @return a changed version of {@code diagram} + */ + public Diagram edit(Frame frame, Collection<String> existingTemplates, Diagram diagram); + + /** + * Templates editor methods are going to be called by the user via a menu item. This method + * returns the label {@code createNew} menu item. + * + * @return a label for the menu item which triggers the creation of a new template through this + * template editor + */ + public String getLabelForNew(); + + /** + * Templates editor methods are going to be called by the user via a menu item. This method + * returns the label {@code edit} menu item. + * + * @return a label for the menu item which triggers the editing of a new template through this + * template editor + */ + public String getLabelForEdit(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessFilter.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,201 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.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; + +/** + * Base class for an awareness filter. It provides basic functionalities + * for properties handling (save to a file, show edit dialog) + */ +public abstract class AwarenessFilter { + /** + * The constructor to be called by sub classes of this class. The filter is built + * from a {@code SetProperties} previously saved on a file. If the file doesn't exist + * a new empty {@code SetProperties} is created. + * + * @param propertiesFilePath the path to the directory where the properties file is located. + * @param propertiesFileName the name of the properties file + * + * @throws IOException if I/O problems occurred when retrieving the properties file + */ + protected 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 uk.ac.qmul.eecs.ccmi.checkboxtree.CheckBoxTree + * + * @return the name of the XML file + */ + protected abstract String getXMLFileName(); + + /** + * Returns the title of the dialog displayed when {@link #showDialog(Component)} + * is called. + * + * @return a title for the preferences dialog + */ + protected abstract String getDialogTitle(); + + /** + * Saves the properties with a custom comment. Subclasses must implement + * this method providing the custom comment. + * + * @param parentComponent the parent component of the displayed dialog + */ + public abstract void saveProperties(Component parentComponent); + + /** + * Brings up a dialog with a {@code CheckBoxTree} that can be used to set the properties of + * this filter. Once the user presses the OK button, the selected properties are saved. + * + * @param parent the parent component of the displayed dialog + */ + 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; + } + } + + /** + * Saves the properties to a file + * + * @param parentComponent a parent component where a message dialog will be displayed + * if an I/O error occur when saving the file + * @param comments additional comments to add at the beginning of the file. + */ + protected 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()) + ); + } + } + + /** + * An instance of {@code SetProperties} where the user configuration + * is saved. + */ + protected final SetProperties properties; + private boolean configurationHasChanged; + private File propertiesFile; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessFilter.properties Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,36 @@ +tree_root=Awareness Filter +broadcast.properties.file_name=broadcast.txt +broadcast.properties.comments=Awareness message broadcast configuration file. Generated automatically. DO NOT EDIT! + +display.properties.file_name=display.txt +display.properties.comments=Awareness message display configuration file. Generated automatically. DO NOT EDIT! + +error.no_properties_dir=Could not open the library directory ("ccmi_editor_data/libs") +error.write_file=Error while saving awareness configuration: {0} + +dialog.display.title=Display Filter Dialog +dialog.broadcast.title=Broadcast Filter Dialog + + +action.text.add_node=added node{0} +action.text.add_edge=added edge{0} +action.text.remove_node=removing node{0} +action.text.remove_edge=removing edge{0} +action.text.edit_node=editing node{0} +action.text.edit_edge=editing edge{0} +action.text.move_node=moving node{0} +action.text.move_edge=moving edge{0} +action.text.select_node=selected node{0} +action.text.unselect_node=unselected node{0} + +action.text.add_node.verb=added node{0} +action.text.add_edge.verb=added edge{0} +action.text.remove_node.verb=is removing node{0} +action.text.remove_edge.verb=is removing edge{0} +action.text.edit_node.verb=is editing node{0} +action.text.edit_edge.verb=is editing edge{0} +action.text.move_node.verb=is moving node{0} +action.text.move_edge.verb=is moving edge{0} +action.text.select_node.verb=selected node{0} +action.text.unselect_node.verb=unselected node{0} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessPanel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,78 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.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; + } + + /** + * Returns a reference to the bottom pane where the name of the users + * is displayed + * + * @return an awareness text pane + */ + AwarenessTextPane getUsersPane() { + return usersPane; + } + + /** + * Returns a reference to the top pane where the name of the users + * is displayed + * + * @return an awareness text pane + */ + AwarenessTextPane getRecordsPane() { + return recordsPane; + } + + /** + * Returns the name of the diagram this panel is bound to + * + * @return the name of the diagram this panel is bound to. + */ + public String getDiagramName(){ + return diagramName; + } + + private AwarenessTextPane usersPane; + private AwarenessTextPane recordsPane; + private String diagramName; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessPanelEditor.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,168 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui.awareness; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.Timer; + +import uk.ac.qmul.eecs.ccmi.network.AwarenessMessage; + +public class AwarenessPanelEditor { + /** + * Creates a new editor + */ + public AwarenessPanelEditor() { + awarenessPanels = Collections.synchronizedList(new ArrayList<AwarenessPanel>()); + } + + /** + * Adds a new panel to the editor. {@code getDiagramName} of {@code panel} + * will be used to retrieve the panel when a change is done to it. + * + * @param panel the panel to add + */ + public void addAwarenessPanel(AwarenessPanel panel){ + awarenessPanels.add(panel); + } + + /** + * Removes a panel from the editor + * @param panel the panel to remove + */ + public void removeAwarenessPanel(AwarenessPanel panel){ + awarenessPanels.remove(panel); + } + + /** + * Replaces a user's user name with a new one. + * + * @param diagramName the name of the diagram linked to the panel + * 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'); + } + + /** + * Removes a user name + * @param diagramName the name of the diagram linked to the panel + * the update has to be performed on + * @param userName the name to be removed + */ + public void removeUserName(String diagramName, String userName){ + AwarenessPanel panel = findPanel(diagramName); + if(panel != null) + panel.getUsersPane().remove(userName+'\n'); + } + + /** + * Adds a new record to the panel + * + * @param diagramName the name of diagram linked to the panel the record + * has to be added to + * @param record the record to add + */ + public void addRecord(String diagramName, String record){ + AwarenessPanel panel = findPanel(diagramName); + if(panel == null) + return; + panel.getRecordsPane().insert(record); + } + + /** + * Adds a record that will automatically be removed after TIMER_DELAY millisecond + * @param diagramName the name of diagram linked to the panel the record + * has to be added to + * @param record the record to add + */ + 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(); + } + + /** + * Removes a record from the panel + * + * @param diagramName the name of diagram linked to the panel the record + * has to be removed from + * @param record the record to add + */ + public void removeRecord(String diagramName, String record){ + AwarenessPanel panel = findPanel(diagramName); + if(panel == null) + return; + panel.getRecordsPane().remove(record); + } + + /** + * Removes all the record from a panel + * @param diagramName the name of diagram linked to the panel the records + * have to be removed from + */ + public void clearRecords(String diagramName){ + AwarenessPanel panel = findPanel(diagramName); + if(panel == null) + return; + panel.getRecordsPane().clear(); + } + + private AwarenessPanel findPanel(String diagramName){ + // it's a synchronized collection, this will synchronize with add and remove + synchronized(awarenessPanels){ + for(AwarenessPanel p : awarenessPanels){ + if(p.getDiagramName().equals(diagramName)){ + return p; + } + } + } + return null; + } + + private List<AwarenessPanel> awarenessPanels; + /** + * The time (in milliseconds) a {@code TimedRecord} will stay in the panel + */ + public static final int TIMER_DELAY = 2000; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/AwarenessTextPane.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,82 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.awareness; + +import java.awt.Component; +import java.awt.event.KeyEvent; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.JTextPane; +import javax.swing.KeyStroke; +import javax.swing.text.DefaultEditorKit; + +import uk.ac.qmul.eecs.ccmi.main.DiagramEditorApp; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +@SuppressWarnings("serial") +class AwarenessTextPane extends JTextPane { + AwarenessTextPane(String accessibleName){ + records = new LinkedList<String>(); + addKeyListener(SpeechUtilities.getSpeechKeyListener(false,true)); + /* prevents getText() from automatically turn all the \n to \r\n */ + getDocument().putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n"); + getAccessibleContext().setAccessibleName(accessibleName); + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB,0), "none"); + SpeechUtilities.changeTabListener(this,DiagramEditorApp.getFrame()); + } + + void insert(String record){ + records.add(0, record); + update(); + } + + void remove(String record){ + records.remove(record); + update(); + } + + void clear(){ + records.clear(); + update(); + } + + protected void processKeyEvent(KeyEvent e){ + if(e.getKeyCode() != KeyEvent.VK_UP && + e.getKeyCode() != KeyEvent.VK_DOWN && + e.getKeyCode() != KeyEvent.VK_RIGHT && + e.getKeyCode() != KeyEvent.VK_LEFT && + e.getKeyCode() != KeyEvent.VK_TAB) + return; + super.processKeyEvent(new KeyEvent((Component)e.getSource(),e.getID(), + e.getWhen(),0,e.getKeyCode(),e.getKeyChar())); + } + + private void update(){ + StringBuilder builder = new StringBuilder(); + for(String r : records){ + builder.append(r); + } + selectAll(); + replaceSelection(builder.toString()); + } + + private List<String> records; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/BroadcastFilter.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,193 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui.awareness; + +import java.awt.Component; +import java.io.IOException; +import java.util.ResourceBundle; + +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/** + * A filter for {@code DiagramEventActionSource} generated by a client and to be broadcasted + * by the server to the other connected clients. The user (running the server) can select on a dialog + * the cases in which the {@code DiagramEventActionSource} is to be broadcasted or not. + * User selections are saved persistently using the persistence functionalities of {@code SetProperties}. + * + * @see uk.ac.qmul.eecs.ccmi.checkboxtree.SetProperties + */ +public class BroadcastFilter extends AwarenessFilter { + /** + * Creates a new instance of this class. Successive calls on this method + * will create a new filter, which will be returned by {@code getInstance} from then on + * + * @return the instance created + * @throws IOException if I/O problems occurred when retrieving the properties file + */ + public static BroadcastFilter createInstance() throws IOException { + broadcastFilter = new BroadcastFilter(); + return broadcastFilter; + } + + /** + * Returns the instance created by the last call of {@code createInstance} + * @return the instance of this class + */ + public static BroadcastFilter getInstance(){ + 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 final String getXMLFileName(){ + return "BroadcastFilterTree.xml"; + } + + @Override + protected final 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") + ); + } + + /** + * Accept or refuse for broadcasting an action generated remotely by a client. + * The local user can determine which actions must be filtered out via the dialog + * displayed by {@code showDialog}. + * + * @param action the action to filter + * @return whether the action can be broadcasted to other client + */ + 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; + } + + /** + * Process for broadcasting an action generated remotely by a client. + * The local user can determine which parts of the action can be excluded + * via the dialog displayed by {@code showDialog}. + * + * @param action the action to filter + * @return whether the action can be broadcasted to other client + */ + public DiagramEventActionSource process(DiagramEventActionSource action){ + /* delete the timestamp if the user decided not to broadcast it */ + if(properties.contains("Broadcast Filter.When.Active History")) + action.setTimestamp(System.currentTimeMillis()); + else + action.setTimestamp(0); + + /* delete the user id if the user decided not to broadcast it */ + if(!properties.contains("Broadcast Filter.Who.User Id")) + action.setUserName(""); + + /* delete the element id if the user decided not to broadcast it */ + switch(action.getCmd()){ + case INSERT_NODE : + case REMOVE_NODE : + case TRANSLATE_NODE : + case SET_NODE_NAME : + case STOP_NODE_MOVE : + case SET_PROPERTY : + case SET_PROPERTIES : + case ADD_PROPERTY : + case REMOVE_PROPERTY : + case SET_MODIFIERS : + if(!properties.contains("Broadcast Filter.What (Object).Which Node")) + action.setElementID(Node.NO_ID); + break; + case INSERT_EDGE : + case SELECT_NODE_FOR_EDGE_CREATION : + case UNSELECT_NODE_FOR_EDGE_CREATION : + case REMOVE_EDGE : + case SET_ENDDESCRIPTION : + case SET_ENDLABEL : + case SET_EDGE_NAME : + case TRANSLATE_EDGE : + case BEND : + case STOP_EDGE_MOVE : + if(!properties.contains("Broadcast Filter.What (Object).Which Edge")) + action.setElementID(Edge.NO_ID); + break; + } + return action; + } + + private static BroadcastFilter broadcastFilter; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/BroadcastFilterTree.xml Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> + +<unselectable value="Broadcast Filter"> + + <selectable value="Who"> + <selectable value="User Id"/> + </selectable> + + <selectable value="What (Action)"> + <selectable value="Node Add"/> + <selectable value="Node Remove"/> + <selectable value="Node Edited"/> + <selectable value="Node Moved"/> + <selectable value="Edge Add"/> + <selectable value="Edge Remove"/> + <selectable value="Edge Edited"/> + <selectable value="Edge Moved"/> + </selectable> + + <selectable value="What (Object)"> + <selectable value="Which Node"/> + <selectable value="Which Edge"/> + </selectable> + + <selectable value="Where (Object)"> + <selectable value="Edge Coordinates" /> + </selectable> + + <selectable value="Where (Source)"> + <selectable value="Audio View"/> + <selectable value="Haptic View"/> + <selectable value="Graphic View"/> + </selectable> + + <selectable value="When"> + <selectable value="Active History"/> + </selectable> + +</unselectable> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/DisplayFilter.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,195 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.gui.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; + +/** + * A filter for {@code DiagramEventActionSource} generated remotely and to be displayed + * both visually and via a text to speech sinthesizer. The user can select on a dialog + * the cases in which the {@code DiagramEventActionSource} is to be displayed or not. + * User selections are saved persistently using the persistence functionalities of {@code SetProperties}. + * + * @see uk.ac.qmul.eecs.ccmi.checkboxtree.SetProperties + */ +public class DisplayFilter extends AwarenessFilter { + /** + * Creates a new instance of this class. Successive calls on this method will return + * the filter already created rather than creating anew one. + * + * @return the instance created + * @throws IOException if I/O problems occurred when retrieving the properties file + */ + public static DisplayFilter createInstance() throws IOException{ + if(displayFilter == null) + displayFilter = new DisplayFilter(); + return displayFilter; + } + + /** + * Returns the unique instance of this class. + * + * @return the unique instance of this class or {@code null} if {@code createInstance} has never + * been called before. + */ + 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 final String getXMLFileName() { + return "DisplayFilterTree.xml"; + } + + @Override + protected final String getDialogTitle(){ + return resources.getString("dialog.display.title"); + } + + @Override + public void saveProperties(Component parentComponent) { + super.saveProperties(parentComponent, resources.getString("display.properties.comments")); + } + + /** + * Returns a string to be uttered by the narrator starting from the action + * that the user is to be made aware of + * + * @param actionSource the action source describing a remote action that the local + * user has to be made aware of + * @return a string to be uttered by a {@code Narrator} + */ + public String processForSpeech(DiagramEventActionSource actionSource){ + return process(actionSource,"Speech"); + } + + /** + * Returns a string to be inserted on the awareness panel starting from the action + * that the user is to be made aware of + * + * @param actionSource the action source describing a remote action that the local + * user has to be made aware of + * @return a string to be inserted in the awareness panel + */ + 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; + private ResourceBundle resources; +} + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/awareness/DisplayFilterTree.xml Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> + +<unselectable value="Display Filter"> + <selectable value="Who"> + <selectable value="Text"/> + <selectable value="Color"/> + <selectable value="Speech"/> + </selectable> + + <selectable value="What (Action)"> + <selectable value="Text"/> + <selectable value="Speech"/> + </selectable> + + <selectable value="What (Object)"> + <selectable value="Text"/> + <selectable value="Color"/> + <selectable value="Speech"/> + <selectable value="Non speech sound"/> + </selectable> +</unselectable> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileChooser.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,55 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.awt.Component; +import java.io.File; + +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; + +/** + * A Component implementing this interface provides the basic file chooser functionality needed to + * in the CCmI Editor. + * + * + * @see JFileChooser + */ +public interface FileChooser { + public void setSelectedFile(File file); + + public File getSelectedFile(); + + public void setFileFilter(FileFilter filter); + + public void resetChoosableFileFilters(); + + public void setCurrentDirectory(File dir); + + public File getCurrentDirectory(); + + public int showOpenDialog(Component parent); + + public int showSaveDialog(Component parent); + + int APPROVE_OPTION = JFileChooser.APPROVE_OPTION; + + int CANCEL_OPTION = JFileChooser.CANCEL_OPTION; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileChooserFactory.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,44 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import javax.swing.JFileChooser; + +/** + * + * A Factory class which creates instances of the {@link FileChooser} interface. + * + */ +public abstract class FileChooserFactory { + public static FileChooser getFileChooser(boolean accessible){ + if(accessible) + return new SpeechFileChooser(); + else + return new NormalFileChooser(); + } + + /** + * Adapter class to get JFileChooser to implement the FileChooser Interface + * + */ + @SuppressWarnings("serial") + private static class NormalFileChooser extends JFileChooser implements FileChooser{ + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileSystemTree.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,297 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.filechooser.FileFilter; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; + +/* + * + * A JTree displaying the content for the local file system. + * + * + */ +@SuppressWarnings("serial") +class FileSystemTree extends JTree { + FileSystemTree(FileFilter filter){ + super(new DefaultTreeModel(FileSystemTreeNode.getRootNode(filter))); + getAccessibleContext().setAccessibleName(ResourceBundle.getBundle(SpeechFileChooser.class.getName()).getString("tree.accessible_name")); + getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + setCellRenderer(new FileSystemTreeCellRendered(getCellRenderer())); + + setSelectionPath(new TreePath(getModel().getRoot())); + overwriteTreeKeystrokes(); + this.addTreeSelectionListener(new TreeSelectionListener(){ + @Override + public void valueChanged(TreeSelectionEvent evt) { + if(treeSelectionListenerGateOpen){ + FileSystemTreeNode treeNode = (FileSystemTreeNode)evt.getPath().getLastPathComponent(); + NarratorFactory.getInstance().speak(treeNode.spokenText()); + } + } + }); + } + + @Override + public void setSelectionPath(TreePath path){ + super.setSelectionPath(path); + scrollPathToVisible(path); + getSelectionPath(); + } + + public void setSelectionPath(File file){ + if(file == null) + return; + + try { + file = file.getCanonicalFile(); + } catch (IOException e) { + setSelectionPath(new TreePath(getModel().getRoot())); + return; + } + /* make a file path: a list of file's each one representing a directory of file's path */ + LinkedList<File> filePath = new LinkedList<File>(); + filePath.add(file); + File parent = file.getParentFile(); + while(parent != null){ + filePath.add(0, parent); + parent = parent.getParentFile(); + } + /* make a TreePath out of the file path */ + FileSystemTreeNode currentNode = (FileSystemTreeNode)getModel().getRoot(); + TreePath treePath = new TreePath(currentNode); + for(File f : filePath){ + boolean found = false; + for(int i=0;i<currentNode.getChildCount();i++){ + if(currentNode.getChildAt(i).getFile().equals(f)){ + currentNode = currentNode.getChildAt(i); + treePath = treePath.pathByAddingChild(currentNode); + found = true; + break; + } + } + if(!found) + break; + } + treeSelectionListenerGateOpen = false; + setSelectionPath(treePath); + treeSelectionListenerGateOpen = true; + } + + public void applyFilter(FileFilter filter){ + FileSystemTreeNode selectedNode = (FileSystemTreeNode)getSelectionPath().getLastPathComponent(); + File file = selectedNode.getFile(); + treeSelectionListenerGateOpen = false; + ((DefaultTreeModel)getModel()).setRoot(FileSystemTreeNode.getRootNode(filter)); + treeSelectionListenerGateOpen = true; + if(file == null) + setSelectionPath(new TreePath(getModel().getRoot())); + else + setSelectionPath(file); + } + + @Override + protected void processMouseEvent(MouseEvent e){ + //do nothing as the tree does not have to be editable with mouse + } + + private void overwriteTreeKeystrokes() { + /* overwrite the keys. up and down arrow are overwritten so that it loops when the top and the */ + /* bottom are reached rather than getting stuck */ + + /* Overwrite keystrokes up,down,left,right arrows and space, shift, ctrl */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0),"down"); + getActionMap().put("down", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FileSystemTreeNode treeNode = (FileSystemTreeNode)getLastSelectedPathComponent(); + /* look if we've got a sibling node after (we are not at the bottom) */ + FileSystemTreeNode nextTreeNode = treeNode.getNextSibling(); + SoundEvent loop = null; + if(nextTreeNode == null){ + TreeNode parent = treeNode.getParent(); + if(parent == null) /* root node, just stay there */ + nextTreeNode = treeNode; + else /* loop = go to first child of own parent */ + nextTreeNode = (FileSystemTreeNode)parent.getChildAt(0); + loop = SoundEvent.LIST_BOTTOM_REACHED; + } + + final String speech = nextTreeNode.spokenText(); + treeSelectionListenerGateOpen = false; + setSelectionPath(new TreePath(nextTreeNode.getPath())); + treeSelectionListenerGateOpen = true; + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + NarratorFactory.getInstance().speak(speech); + } + }); + }}); + + /* Overwrite keystrokes up,down,left,right arrows and space, shift, ctrl */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0),"up"); + getActionMap().put("up", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FileSystemTreeNode treeNode = (FileSystemTreeNode)getLastSelectedPathComponent(); + /* look if we've got a sibling node after (we are not at the bottom) */ + FileSystemTreeNode peviousTreeNode = treeNode.getPreviousSibling(); + SoundEvent loop = null; + if(peviousTreeNode == null){ + TreeNode parent = treeNode.getParent(); + if(parent == null) /* root node, just stay there */ + peviousTreeNode = treeNode; + else /* loop = go to first child of own parent */ + peviousTreeNode = (FileSystemTreeNode)parent.getChildAt(parent.getChildCount()-1); + loop = SoundEvent.LIST_TOP_REACHED; + } + + final String speech = peviousTreeNode.spokenText(); + treeSelectionListenerGateOpen = false; + setSelectionPath(new TreePath(peviousTreeNode.getPath())); + treeSelectionListenerGateOpen = true; + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + NarratorFactory.getInstance().speak(speech); + } + }); + }}); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0),"left"); + getActionMap().put("left", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + TreeNode treeNode = (TreeNode)path.getLastPathComponent(); + final FileSystemTreeNode parent = (FileSystemTreeNode)treeNode.getParent(); + if(parent == null){/* root node */ + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + else{ + TreePath newPath = new TreePath(parent.getPath()); + treeSelectionListenerGateOpen = false; + setSelectionPath(newPath); + collapsePath(newPath); + treeSelectionListenerGateOpen = true; + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_COLLAPSE,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(parent.spokenText()); + } + }); + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0),"right"); + getActionMap().put("right", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + TreeNode treeNode = (TreeNode)path.getLastPathComponent(); + if(treeNode.isLeaf()){/* leaf node */ + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + else{ + expandPath(path); + final FileSystemTreeNode firstChild = (FileSystemTreeNode)treeNode.getChildAt(0); + treeSelectionListenerGateOpen = false; + setSelectionPath(new TreePath(firstChild.getPath())); + treeSelectionListenerGateOpen = true; + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_EXPAND,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(firstChild.spokenText()); + } + }); + } + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,0),"space"); + getActionMap().put("space",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + FileSystemTreeNode treeNode = (FileSystemTreeNode)path.getLastPathComponent(); + NarratorFactory.getInstance().speak(treeNode.toString()); + } + }); + + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,InputEvent.CTRL_DOWN_MASK), "ctrl_space"); + getActionMap().put("ctrl_space", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = getSelectionPath(); + FileSystemTreeNode treeNode = (FileSystemTreeNode)path.getLastPathComponent(); + NarratorFactory.getInstance().speak(treeNode.getFile().getPath()); + } + }); + /* make the tree ignore the page up and page down keys */ + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,0),"none"); + getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,0),"none"); + } + private boolean treeSelectionListenerGateOpen; +} + +/** + * This class overwrites the default cell renderer in order to always render directories with a + * directory-icon regardless whether they are a leaf node or not. + * + */ +class FileSystemTreeCellRendered implements TreeCellRenderer { + FileSystemTreeCellRendered(TreeCellRenderer delegate){ + this.delegate = delegate; + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, + boolean expanded, boolean leaf, int row, boolean hasFocus) { + if(leaf && ((FileSystemTreeNode)value).getFile().isDirectory() ) + return delegate.getTreeCellRendererComponent(tree, value, selected, expanded, false, row, hasFocus); + return delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + } + + TreeCellRenderer delegate; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/FileSystemTreeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,223 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.io.File; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.ResourceBundle; + +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileSystemView; +import javax.swing.tree.TreeNode; + +/* + * The tree nodes of a FileSystemTree. Each nodes represent either a directory or a file + * in the local file system where the CCmI Editor is executed. + * + */ +class FileSystemTreeNode implements TreeNode { + + public static FileSystemTreeNode getRootNode(FileFilter filter){ + return new FileSystemTreeNode(filter){ + @Override + public boolean getAllowsChildren(){ + return true; + } + @Override + public String toString(){ + return ResourceBundle.getBundle(SpeechFileChooser.class.getName()).getString("tree_root.label"); + } + }; + } + + private FileSystemTreeNode(FileFilter filter){ // constructor for root + fileSystemView = FileSystemView.getFileSystemView(); + parent = null; + this.filter = filter; + containedFiles = File.listRoots(); + + children = new FileSystemTreeNode[containedFiles.length]; + for(int i=0; i< children.length; i++){ + children[i] = new FileSystemTreeNode(this, containedFiles[i],filter); + children[i].isFileSystemRoot = true; + } + } + + private FileSystemTreeNode(TreeNode parent, File file, FileFilter filter){ + fileSystemView = FileSystemView.getFileSystemView(); + this.file = file; + this.parent = parent; + this.filter = filter; + + if(file.isDirectory()){ + containedFiles = file.listFiles(); + if(containedFiles == null ) + containedFiles = new File[0]; + ArrayList<File> fileList = new ArrayList<File>(containedFiles.length); + for(File f : containedFiles){ + if(f.isDirectory() || filter.accept(f)) + fileList.add(f); + } + Collections.sort(fileList,new FileSystemComparator()); + containedFiles = new File[fileList.size()]; + containedFiles = fileList.toArray(containedFiles); + }else{ + containedFiles = new File[0]; + } + children = new FileSystemTreeNode[containedFiles.length]; + } + + public FileSystemTreeNode getNextSibling(){ + if(parent == null) + return null; + int thisIndex = parent.getIndex(this); + int numChildren = parent.getChildCount(); + if(thisIndex == numChildren-1) + return null; + return (FileSystemTreeNode)parent.getChildAt(thisIndex+1); + } + + public FileSystemTreeNode getPreviousSibling(){ + if(parent == null) + return null; + int thisIndex = parent.getIndex(this); + if(thisIndex == 0) + return null; + return (FileSystemTreeNode)parent.getChildAt(thisIndex-1); + } + + public TreeNode[] getPath(){ + List<TreeNode> pathList = new LinkedList<TreeNode>(); + pathList.add(0, this); + TreeNode parent = getParent(); + while(parent != null){ + pathList.add(0,parent); + parent = parent.getParent(); + } + TreeNode[] path = new TreeNode[pathList.size()]; + return pathList.toArray(path); + } + + public File getFile(){ + return file; + } + + public String spokenText(){ + String fileType = "dir"; + if(file != null && file.isFile()) + fileType = "file"; + return MessageFormat.format( + ResourceBundle.getBundle(SpeechFileChooser.class.getName()).getString(fileType), + toString() + ); + } + + @Override + public String toString(){ + if(isFileSystemRoot){ + String name = fileSystemView.getSystemDisplayName(file); + if(name.isEmpty()) + name = file.toString(); + return name; + } + return file.getName(); + } + + /* --- TREE NODE INTEFACE IMPLEMENTATION --- */ + @Override + public Enumeration<FileSystemTreeNode> children() { + return new Enumeration<FileSystemTreeNode>(){ + @Override + public boolean hasMoreElements() { + return (index < children.length); + } + + @Override + public FileSystemTreeNode nextElement() { + return new FileSystemTreeNode(parent, containedFiles[index++],filter); + } + int index = 0; + }; + } + + @Override + public boolean getAllowsChildren() { + return file.isFile(); + } + + @Override + public FileSystemTreeNode getChildAt(int index) throws IndexOutOfBoundsException { + /* builds the children lazily */ + if(children[index] == null) + children[index] = new FileSystemTreeNode(this, containedFiles[index],filter); + return children[index]; + } + + @Override + public int getChildCount() { + return containedFiles.length; + } + + @Override + public int getIndex(TreeNode n) { + FileSystemTreeNode fileSystemTreeNode = (FileSystemTreeNode)n; + for(int i=0; i< containedFiles.length; i++){ + if(fileSystemTreeNode.file.equals(containedFiles[i])) + return i; + } + return -1; + } + + @Override + public TreeNode getParent() { + return parent; + } + + @Override + public boolean isLeaf() { + return (containedFiles.length == 0); + } + + private File[] containedFiles; + private File file; + private FileSystemTreeNode[] children; + private TreeNode parent; + private FileSystemView fileSystemView; + private FileFilter filter; + private boolean isFileSystemRoot; +} + +class FileSystemComparator implements Comparator<File>{ + @Override + public int compare(File f1, File f2) { + if(f1.isDirectory() && f2.isFile()) + return -1; + else if(f1.isFile() && f2.isDirectory()) + return 1; + else + return f1.compareTo(f2); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,252 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import java.awt.Component; +import java.awt.GridBagLayout; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.io.File; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.filechooser.FileFilter; + +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; + +/* + * An accessible file chooser. Users can browse the file system only using hearing as + * in the same fashion as they do when exploring a diagram. + * + */ +@SuppressWarnings("serial") +class SpeechFileChooser extends JPanel implements FileChooser { + SpeechFileChooser(){ + super(new GridBagLayout()); + initComponents(); + addComponents(); + } + + private void addComponents() { + GridBagUtilities gridBag = new GridBagUtilities(); + JScrollPane scrollPane = new JScrollPane(tree); + add(scrollPane,gridBag.all()); + add(fileNameLabel,gridBag.label()); + add(fileNameTextField, gridBag.field()); + add(fileTypeLabel, gridBag.label()); + add(fileTypeComboBox, gridBag.field()); + } + + private void initComponents(){ + resources = ResourceBundle.getBundle(this.getClass().getName()); + /* file name components */ + fileNameLabel = new JLabel(resources.getString("dialog.file_chooser.file_name")); + fileNameTextField = new TreeSelectionTextField(); + fileNameTextField.setColumns(20); + fileNameLabel.setLabelFor(fileNameTextField); + fileNameTextField.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + fileTypeLabel = new JLabel(resources.getString("dialog.file_chooser.file_type")); + + /* file type components */ + Object [] items = {new AllFileFilter(resources.getString("all_file_filter.label"))}; + fileTypeComboBox = new JComboBox(items); + fileTypeLabel.setLabelFor(fileTypeComboBox); + + /* set up the listener binding the tree with the file name and file type components */ + tree = new FileSystemTree((FileFilter)fileTypeComboBox.getSelectedItem()); + tree.addTreeSelectionListener(fileNameTextField); + fileTypeComboBox.addItemListener(new ItemListener(){ + @Override + public void itemStateChanged(ItemEvent evt) { + if(evt.getStateChange() == ItemEvent.SELECTED) + tree.applyFilter((FileFilter)evt.getItem()); + } + }); + } + + @Override + public void setSelectedFile(File file){ + selectedFile = file; + if(file == null) + return; + tree.setSelectionPath(file); + } + + @Override + public File getSelectedFile(){ + return selectedFile; + } + + @Override + public void setFileFilter(FileFilter filter){ + if(filter != null){ + FileFilter wrap = new WrapFileFilter(filter); + ((DefaultComboBoxModel)fileTypeComboBox.getModel()).insertElementAt(wrap, 0); + fileTypeComboBox.getModel().setSelectedItem(wrap); + } + } + + @Override + public void resetChoosableFileFilters(){ + if(fileTypeComboBox.getItemCount() == 2) + ((DefaultComboBoxModel)fileTypeComboBox.getModel()).removeElementAt(0); + } + + @Override + public void setCurrentDirectory(File dir){ + currentDir = dir; + if(dir == null) + return; + tree.setSelectionPath(dir); + } + + @Override + public File getCurrentDirectory(){ + return currentDir; + } + + @Override + public int showOpenDialog(Component parent){ + FileSystemTreeNode treeNode = (FileSystemTreeNode)tree.getSelectionPath().getLastPathComponent(); + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString("dialog.open.message"), + treeNode.spokenText())); + return showDialog(parent,true); + } + + @Override + public int showSaveDialog(Component parent){ + FileSystemTreeNode treeNode = (FileSystemTreeNode)tree.getSelectionPath().getLastPathComponent(); + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString("dialog.save.message"), + treeNode.spokenText())); + return showDialog(parent,false); + } + + 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, JButton source){ + if(source.equals(getOkButton())){ + if(fileNameTextField.getText().isEmpty()){ + NarratorFactory.getInstance().speak(resources.getString("dialog.error.no_file_name")); + return; + } + } + super.onClose(dialog, source); + } + }; + + 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()); + int result = optionPane.showDialog(parent, this); + fileTypeComboBox.removeItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + + 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 */ + File directory = treeNode.getFile(); + if(!directory.isDirectory()) + directory = directory.getParentFile(); + + selectedFile = new File(directory,fileNameTextField.getText()); + return APPROVE_OPTION; + }else{ + return CANCEL_OPTION; + } + } + + private ResourceBundle resources; + + private JLabel fileNameLabel; + private TreeSelectionTextField fileNameTextField; + private JLabel fileTypeLabel; + private JComboBox fileTypeComboBox; + private FileSystemTree tree; + private File selectedFile; + private File currentDir; +} + +class AllFileFilter extends FileFilter { + AllFileFilter(String description){ + this.description = description; + } + + @Override + public boolean accept(File file) { + return true; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String toString(){ + return description; + } + String description; +} + +class WrapFileFilter extends FileFilter { + WrapFileFilter(FileFilter delegate){ + this.delegate = delegate; + } + + @Override + public boolean accept(File f) { + return delegate.accept(f); + } + + @Override + public String getDescription() { + return delegate.getDescription(); + } + + @Override + public String toString(){ + return delegate.getDescription(); + } + + FileFilter delegate; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/SpeechFileChooser.properties Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,19 @@ + +dir={0} Directory +file={0} + +tree_root.label=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 +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: +dialog.file_chooser.file_type=File Type: +dialog.error.no_file_name=Error: no file name entered +all_file_filter.label=All Files \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/filechooser/TreeSelectionTextField.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,44 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.filechooser; + +import javax.swing.JTextField; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; + +/* + * This text field will display the file name that the user selects when + * browsing the tree with the keyboard arrow keys. + */ +@SuppressWarnings("serial") +class TreeSelectionTextField extends JTextField implements TreeSelectionListener { + + @Override + public void valueChanged(TreeSelectionEvent evt) { + TreePath path = evt.getPath(); + FileSystemTreeNode node = (FileSystemTreeNode)path.getLastPathComponent(); + + if(node.getFile() != null && node.getFile().isFile()) + setText(node.toString()); + else + setText(""); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/license.txt Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PersistenceManager.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,579 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.persistence; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import javax.swing.tree.TreeNode; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.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; + +/** + * The PersistanceManager provides methods for saving and retrieving diagrams from an XML + * file. Both templates diagrams (prototypes from which actual diagram instances are created + * through cloning) and diagram instances can be saved to a file. The tag name used in the XML + * file can be accessed via the static {@code String} variables of this class. + * + */ +public abstract class PersistenceManager { + /** + * Encodes a diagram template in a file in XML format + * + * @param diagram the diagram to be encoded + * @param file the file where the diagram is going to be encoded + * @throws IOException if there are any I/O problems with the file + */ + public static void encodeDiagramTemplate(Diagram diagram, File file) throws IOException{ + ResourceBundle resources = ResourceBundle.getBundle(PersistenceManager.class.getName()); + if(file.createNewFile() == false) + throw new IOException(resources.getString("dialog.error.file_exists")); + + DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = null; + try { + docBuilder = dbfac.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new IOException(resources.getString("dialog.error.problem.save"),e); + } + Document doc = docBuilder.newDocument(); + + Element root = doc.createElement(DIAGRAM); + doc.appendChild(root); + /* diagram name and prototypePersstenceDelegate */ + root.setAttribute(NAME, diagram.getName()); + root.setAttribute(PROTOTYPE_PERSISTENCE_DELEGATE, diagram.getPrototypePersistenceDelegate().getClass().getName()); + + writePrototypes(doc, root, diagram); + + //set up a transformer + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer trans = null; + try { + trans = transfac.newTransformer(); + } catch (TransformerConfigurationException tce) { + throw new IOException(resources.getString("dialog.error.problem.save"),tce); + } + trans.setOutputProperty(OutputKeys.INDENT, "yes"); + trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(2)); + + StreamResult result = new StreamResult(new BufferedWriter(new FileWriter(file))); + DOMSource source = new DOMSource(doc); + try { + trans.transform(source, result); + } catch (TransformerException te) { + throw new IOException(resources.getString("dialog.error.problem.save"),te); + } + } + + /** + * Decodes a diagram template from a file in XML format + * + * @param XMLFile the file to read the diagram from + * @throws IOException if there are any I/O problems with the file + * + * @return the diagram encoded in {@code XMLFile} + */ + public static Diagram decodeDiagramTemplate(File XMLFile) throws IOException{ + ResourceBundle resources = ResourceBundle.getBundle(PersistenceManager.class.getName()); + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = null; + try { + dBuilder = dbFactory.newDocumentBuilder(); + } catch (ParserConfigurationException pce) { + throw new IOException(resources.getString("dialog.error.problem.open"),pce); + } + Document doc = null; + try { + doc = dBuilder.parse(XMLFile); + } catch (SAXException se) { + throw new IOException(resources.getString("dialog.error.problem.open"),se); + } + doc.getDocumentElement().normalize(); + + if(doc.getElementsByTagName(DIAGRAM).item(0) == null) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element root = (Element)doc.getElementsByTagName(DIAGRAM).item(0); + String diagramName = root.getAttribute(NAME); + if(diagramName.isEmpty()) + throw new IOException(resources.getString("dialog.error.malformed_file")); + String persistenceDelegateClassName = root.getAttribute(PROTOTYPE_PERSISTENCE_DELEGATE); + PrototypePersistenceDelegate persistenceDelegate = null; + try{ + Class<? extends PrototypePersistenceDelegate> c = Class.forName(persistenceDelegateClassName).asSubclass(PrototypePersistenceDelegate.class); + persistenceDelegate = c.newInstance(); + }catch(Exception e){ + throw new IOException(resources.getString("dialog.error.problem.open"),e); + } + + final List<Node> nList = readNodePrototypes(doc,persistenceDelegate); + final List<Edge> eList = readEdgePrototypes(doc,persistenceDelegate); + Node[] nArray = new Node[nList.size()]; + Edge[] eArray = new Edge[eList.size()]; + return Diagram.newInstance(diagramName,nList.toArray(nArray),eList.toArray(eArray),persistenceDelegate); + } + + /** + * Encodes a diagram instance into the given output stream. Using output stream + * instead of {@code Writer} as it's advised by the <i>StreamResult API</i> + * @see http://download.oracle.com/javase/6/docs/api/javax/xml/transform/stream/StreamResult.html + * + * @param diagram the diagram to encode + * @param newName the new name of the diagram to encode. This will also be the name of the file but {@code diagram} + * will still keep the old name, that is the value returned by {@code getName()} won't be changed to {@code newName}. + * If a {@code null} value is passed than the value returned by {@code diagram.getName()} will be used. + * + * @param out where the diagram will be encoded + * @throws IOException if there are any I/O problems with the file + */ + public static void encodeDiagramInstance(Diagram diagram, String newName, OutputStream out) throws IOException{ + ResourceBundle resources = ResourceBundle.getBundle(PersistenceManager.class.getName()); + DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = null; + try { + docBuilder = dbfac.newDocumentBuilder(); + } catch (ParserConfigurationException pce) { + throw new IOException(resources.getString("dialog.error.problem.save"),pce); + } + Document doc = docBuilder.newDocument(); + + Element root = doc.createElement(DIAGRAM); + root.setAttribute(NAME, (newName != null) ? newName : diagram.getName()); + root.setAttribute(PROTOTYPE_PERSISTENCE_DELEGATE, diagram.getPrototypePersistenceDelegate().getClass().getName()); + doc.appendChild(root); + + /* store bookmarks */ + Element bookmarksTag = doc.createElement(BOOKMARKS); + TreeModel<Node,Edge> treeModel = diagram.getTreeModel(); + for(String key : treeModel.getBookmarks()){ + Element bookmarkTag = doc.createElement(BOOKMARK); + bookmarkTag.setAttribute(KEY, key); + if(treeModel.getBookmarkedTreeNode(key).isRoot()) + bookmarkTag.setTextContent(ROOT_AS_STRING); + else + bookmarkTag.setTextContent(getTreeNodeAsString(treeModel.getBookmarkedTreeNode(key))); + bookmarksTag.appendChild(bookmarkTag); + } + if(bookmarksTag.hasChildNodes()) + root.appendChild(bookmarksTag); + + /* store notes */ + Element notesTag = doc.createElement(NOTES); + DiagramTreeNode treeRoot = (DiagramTreeNode)diagram.getTreeModel().getRoot(); + for( @SuppressWarnings("unchecked") + Enumeration<DiagramTreeNode> enumeration = treeRoot.depthFirstEnumeration(); enumeration.hasMoreElements();){ + DiagramTreeNode treeNode = enumeration.nextElement(); + if(!treeNode.getNotes().isEmpty()){ + Element noteTag = doc.createElement(NOTE); + Element treeNodeTag = doc.createElement(TREE_NODE); + if(treeNode.isRoot()) + treeNodeTag.setTextContent(ROOT_AS_STRING); + else + treeNodeTag.setTextContent(getTreeNodeAsString(treeNode)); + Element contentTag = doc.createElement(CONTENT); + contentTag.setTextContent(CharEscaper.replaceNewline(treeNode.getNotes())); + noteTag.appendChild(treeNodeTag); + noteTag.appendChild(contentTag); + notesTag.appendChild(noteTag); + } + } + + if(notesTag.hasChildNodes()) + root.appendChild(notesTag); + + writePrototypes(doc,root,diagram); + + Element components = doc.createElement(COMPONENTS); + root.appendChild(components); + + synchronized(diagram.getCollectionModel().getMonitor()){ + Collection<Node> nodes = diagram.getCollectionModel().getNodes(); + Collection<Edge> edges = diagram.getCollectionModel().getEdges(); + + /* store nodes */ + Element nodesTag = doc.createElement(NODES); + components.appendChild(nodesTag); + List<Node> nList = new ArrayList<Node>(nodes); + for(Node n : nList){ + Element nodeTag = doc.createElement(NODE); + nodeTag.setAttribute(ID, String.valueOf(n.getId())); + nodeTag.setAttribute(TYPE, n.getType()); + nodesTag.appendChild(nodeTag); + n.encode(doc, nodeTag); + } + + Element edgesTag = doc.createElement(EDGES); + components.appendChild(edgesTag); + for(Edge e : edges){ + Element edgeTag = doc.createElement(EDGE); + edgesTag.appendChild(edgeTag); + e.encode(doc,edgeTag,nList); + } + } + //set up a transformer + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer trans = null; + try { + trans = transfac.newTransformer(); + } catch (TransformerConfigurationException tec) { + throw new IOException(resources.getString("dialog.error.problem.save")); + } + trans.setOutputProperty(OutputKeys.INDENT, "yes"); + trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(2)); + + + StreamResult result = new StreamResult(out); + DOMSource source = new DOMSource(doc); + try { + trans.transform(source, result); + } catch (TransformerException te) { + throw new IOException(resources.getString("dialog.error.problem.save"),te); + } + } + + /** + * Encodes a diagram instance into the given output stream. Using output stream + * instead of {@code Writer} as it's advised by the <i>StreamResult API</i> + * @see http://download.oracle.com/javase/6/docs/api/javax/xml/transform/stream/StreamResult.html + * + * @param diagram the diagram to encode + * @param out an output stram to the file where the diagram will be encoded + * @throws IOException if there are any I/O problems with the file + */ + public static void encodeDiagramInstance(Diagram diagram, OutputStream out) throws IOException{ + encodeDiagramInstance(diagram,null,out); + } + + /** + * Decodes a diagram instance from the given input stream. Using input stream + * instead of {@code Reader} as it's advised by the <i>StreamResult API</i> + * @see http://download.oracle.com/javase/6/docs/api/javax/xml/transform/stream/StreamResult.html + * + * @param in an input stream to the file the diagram is decoded from + * @throws IOException if there are any I/O problems with the file + * + * @return the diagram encoded in the file + */ + public static Diagram decodeDiagramInstance(InputStream in) throws IOException { + ResourceBundle resources = ResourceBundle.getBundle(PersistenceManager.class.getName()); + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = null; + try { + dBuilder = dbFactory.newDocumentBuilder(); + } catch (ParserConfigurationException pce) { + throw new IOException(resources.getString("dialog.error.problem.open"),pce); + } + Document doc = null; + try { + doc = dBuilder.parse(in); + } catch (SAXException se) { + throw new IOException(resources.getString("dialog.error.problem.open"),se); + } + doc.getDocumentElement().normalize(); + + if(doc.getElementsByTagName(DIAGRAM).item(0) == null) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element root = (Element)doc.getElementsByTagName(DIAGRAM).item(0); + String diagramName = root.getAttribute(NAME); + if(diagramName.isEmpty()) + throw new IOException(resources.getString("dialog.error.malformed_file")); + String persistenceDelegateClassName = root.getAttribute(PROTOTYPE_PERSISTENCE_DELEGATE); + PrototypePersistenceDelegate persistenceDelegate = null; + try{ + Class<? extends PrototypePersistenceDelegate> c = Class.forName(persistenceDelegateClassName).asSubclass(PrototypePersistenceDelegate.class); + persistenceDelegate = c.newInstance(); + }catch(Exception e){ + throw new IOException(resources.getString("dialog.error.problem.open"),e); + } + + final List<Node> nList = readNodePrototypes(doc,persistenceDelegate); + final List<Edge> eList = readEdgePrototypes(doc,persistenceDelegate); + + final Node[] nodes = nList.toArray(new Node[nList.size()]); + final Edge[] edges = eList.toArray(new Edge[eList.size()]); + + Diagram diagram = Diagram.newInstance(diagramName, nodes,edges,persistenceDelegate); + CollectionModel<Node,Edge> collectionModel = diagram.getCollectionModel(); + TreeModel<Node,Edge> treeModel = diagram.getTreeModel(); + + /* a map linking node ids in the XML file to the actual Node object they represent */ + Map<String,Node> nodesId = new LinkedHashMap<String,Node>(); + + if(doc.getElementsByTagName(COMPONENTS).item(0) == null) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element componentsTag = (Element)doc.getElementsByTagName(COMPONENTS).item(0); + NodeList componentsChildren = componentsTag.getChildNodes(); + Element nodesTag = null; + for(int i=0;i<componentsChildren.getLength();i++){ + if(NODES.equals(componentsChildren.item(i).getNodeName())) + nodesTag = (Element)componentsChildren.item(i); + } + + NodeList elemList = nodesTag.getElementsByTagName(NODE); + for(int i=0; i<elemList.getLength();i++){ + Element nodeTag = (Element)elemList.item(i); + String idAsString = nodeTag.getAttribute(ID); + String type = nodeTag.getAttribute(TYPE); + Node prototype = null; + for(Node n : nList) + if(n.getType().equals(type)){ + prototype = n; + break; + } + if(prototype == null) + throw new IOException( + MessageFormat.format( + resources.getString("dialog.error.node_type_not_present"), + type)); + Node node = (Node)prototype.clone(); + + nodesId.put(idAsString, node); + + try { + Long id = Long.valueOf(idAsString); + node.setId(id); + }catch(NumberFormatException nfe){ + throw new IOException(resources.getString("dialog.error.malformed_file"),nfe); + } + collectionModel.insert(node,DiagramEventSource.PERS); + try{ + node.decode(doc, nodeTag); + }catch(IOException ioe){ // just give a message to the exception + throw new IOException(resources.getString("dialog.error.malformed_file"),ioe); + } + } + + Element edgesTag = null; + for(int i=0;i<componentsChildren.getLength();i++) + if(EDGES.equals(componentsChildren.item(i).getNodeName())) + edgesTag = (Element)componentsChildren.item(i); + + elemList = edgesTag.getElementsByTagName(EDGE); + for(int i=0; i<elemList.getLength();i++){ + Element edgeTag = (Element)elemList.item(i); + String type = edgeTag.getAttribute(TYPE); + + Edge prototype = null; + for(Edge e : eList) + if(e.getType().equals(type)){ + prototype = e; + break; + } + if(prototype == null) + throw new IOException(MessageFormat.format( + resources.getString("dialog.error.edge_type_not_present"), + type + )); + + Edge edge = (Edge)prototype.clone(); + + try{ + edge.decode(doc, edgeTag, nodesId); + }catch(IOException ioe){ + throw new IOException(resources.getString("dialog.error.malformed_file"),ioe); + } + collectionModel.insert(edge,DiagramEventSource.PERS); + } + + /* retrieve bookmarks */ + NodeList bookmarkList = root.getElementsByTagName(BOOKMARK); + for(int i=0;i<bookmarkList.getLength();i++){ + Element bookmarkTag = (Element)bookmarkList.item(i); + String key = bookmarkTag.getAttribute(KEY); + if(key.isEmpty()) + throw new IOException(resources.getString("dialog.error.malformed_file")); + String path = bookmarkTag.getTextContent(); + DiagramTreeNode treeNode = getTreeNodeFromString(treeModel,path); + treeModel.putBookmark(key, treeNode,DiagramEventSource.PERS); + } + + /* retrieve notes */ + NodeList noteList = root.getElementsByTagName(NOTE); + for(int i=0;i<noteList.getLength();i++){ + Element noteTag = (Element)noteList.item(i); + if(noteTag.getElementsByTagName(TREE_NODE).item(0) == null ) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element pathTag = (Element)noteTag.getElementsByTagName(TREE_NODE).item(0); + String path = pathTag.getTextContent(); + if(noteTag.getElementsByTagName(CONTENT).item(0) == null) + throw new IOException(resources.getString("dialog.error.malformed_file")); + Element contentTag = (Element)noteTag.getElementsByTagName(CONTENT).item(0); + String content = CharEscaper.restoreNewline(contentTag.getTextContent()); + DiagramTreeNode treeNode = getTreeNodeFromString(treeModel,path); + treeModel.setNotes(treeNode,content,DiagramEventSource.PERS); + } + + /* normally nodes and edges should be saved in order, this is to prevent * + * a manual editing of the xml to affect the program logic */ + collectionModel.sort(); + /* we have to do this has the insertion in the model made it modified */ + collectionModel.setUnmodified(); + return diagram; + } + + private static void writePrototypes(Document doc, Element root, Diagram diagram){ + Node[] nodes = diagram.getNodePrototypes(); + Edge[] edges = diagram.getEdgePrototypes(); + Element components = doc.createElement(PROTOTYPES); + root.appendChild(components); + + PrototypePersistenceDelegate delegate = diagram.getPrototypePersistenceDelegate(); + for(Node n : nodes){ + Element nodeTag = doc.createElement(NODE); + components.appendChild(nodeTag); + delegate.encodeNodePrototype(doc, nodeTag, n); + } + + for(Edge e : edges){ + Element edgeTag = doc.createElement(EDGE); + components.appendChild(edgeTag); + delegate.encodeEdgePrototype(doc, edgeTag, e); + } + } + + private static List<Node> readNodePrototypes(Document doc, PrototypePersistenceDelegate delegate) throws IOException{ + if(doc.getElementsByTagName(PROTOTYPES).item(0) == null) + throw new IOException(ResourceBundle.getBundle(PersistenceManager.class.getName()).getString("dialog.error.malformed_file")); + Element prototypesTag = (Element)doc.getElementsByTagName(PROTOTYPES).item(0); + NodeList elemList = prototypesTag.getElementsByTagName(NODE); + final List<Node> nList = new ArrayList<Node>(elemList.getLength()); + for(int i=0; i<elemList.getLength();i++){ + Element element = (Element)elemList.item(i); + try{ + Node n = delegate.decodeNodePrototype(element); + nList.add(n); + }catch(IOException ioe){ // just set the message for the exception + throw new IOException(ResourceBundle.getBundle(PersistenceManager.class.getName()).getString("dialog.error.malformed_file"),ioe); + } + } + return nList; + } + + private static List<Edge> readEdgePrototypes(Document doc, PrototypePersistenceDelegate delegate) throws IOException{ + if(doc.getElementsByTagName(PROTOTYPES).item(0) == null) + throw new IOException(ResourceBundle.getBundle(PersistenceManager.class.getName()).getString("dialog.error.malformed_file")); + Element prototypesTag = (Element)doc.getElementsByTagName(PROTOTYPES).item(0); + NodeList elemList = prototypesTag.getElementsByTagName(EDGE); + final List<Edge> eList = new ArrayList<Edge>(elemList.getLength()); + for(int i=0; i<elemList.getLength();i++){ + Element element = (Element)elemList.item(i); + try{ + Edge e = delegate.decodeEdgePrototype(element); + eList.add(e); + }catch(IOException ioe){ + throw new IOException(ResourceBundle.getBundle(PersistenceManager.class.getName()).getString("dialog.error.malformed_file"),ioe); + } + } + return eList; + } + + private static String getTreeNodeAsString(DiagramTreeNode treeNode){ + TreeNode[] path = treeNode.getPath(); + StringBuilder builder = new StringBuilder(); + for(int i=0;i<path.length-1; i++) + builder.append(String.valueOf(path[i].getIndex(path[i+1]))).append(' '); + if(builder.toString().endsWith(" ")) + builder.deleteCharAt(builder.length()-1); + return builder.toString(); + + } + + private static DiagramTreeNode getTreeNodeFromString(TreeModel<Node,Edge> model, String path) throws IOException{ + DiagramTreeNode treeNode = (DiagramTreeNode)model.getRoot(); + if(ROOT_AS_STRING.equals(path)) + return treeNode; + String[] nodesAsString = path.split(" "); + + try { + for(String nodeAsString : nodesAsString) + treeNode = (DiagramTreeNode) treeNode.getChildAt(Integer.parseInt(nodeAsString)); + }catch(Exception e){ + throw new IOException(e); + } + return treeNode; + } + + public final static String NAME = "Name"; + public final static String DIAGRAM = "Diagram"; + public final static String PROTOTYPE_PERSISTENCE_DELEGATE = "PrototypeDelegate"; + public final static String COMPONENTS = "Components"; + public final static String PROTOTYPES = "Prototypes"; + public final static String NODE = "Node"; + public final static String NODES = "Nodes"; + public final static String EDGE = "Edge"; + public final static String EDGES = "Edges"; + public final static String POSITION = "Position"; + public final static String PROPERTIES = "Properties"; + public final static String PROPERTY = "Property"; + public final static String TYPE = "Type"; + public final static String VALUE = "Value"; + public final static String ELEMENT = "Element"; + public static final String LABEL = "Label"; + public final static String POINTS = "Points"; + public final static String POINT = "Point"; + public final static String ID = "id"; + public final static String NEIGHBOURS = "Neighbours"; + public static final String MODIFIER = "Modifier"; + public static final String MODIFIERS = "Modifiers"; + public static final String X = "x"; + public static final String Y = "y"; + public static final String BOOKMARKS = "Bookmarks"; + public static final String BOOKMARK = "Bookmark"; + public static final String KEY = "Key"; + public static final String NOTES = "Notes"; + public static final String NOTE = "Note"; + public static final String CONTENT = "Content"; + public static final String TREE_NODE = "TreeNode"; + private static final String ROOT_AS_STRING = "-1"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PersistenceManager.properties Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,8 @@ + + +dialog.error.file_exists=File already exist +dialog.error.node_type_not_present=Node type {0} not present in template definition +dialog.error.edge_type_not_present=Edge type {0} not present in template definition +dialog.error.problem.save=Error: a problem occurred while saving the file +dialog.error.problem.open=Error: a problem occurred while opening the file +dialog.error.malformed_file=Error: the opened file is malformed \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/persistence/PrototypePersistenceDelegate.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,41 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.gui.persistence; + +import java.io.IOException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.Edge; + +/** + * + * Each package providing an implementation of nodes and edges must provide a PrototypePersistenceDelegate + * as well. This class will be used by the PersistanceManager to save and retrieve the information necessary to rebuild + * nodes and edges from an XML file. + */ +public interface PrototypePersistenceDelegate { + public void encodeNodePrototype(Document doc, Element parent, Node n); + public void encodeEdgePrototype(Document doc, Element parent, Edge e); + public Node decodeNodePrototype(Element root) throws IOException; + public Edge decodeEdgePrototype(Element root) throws IOException; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Edge.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,58 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.util.BitSet; + +/* + * A diagram edge in the haptics space. + */ +class Edge { + + public Edge(int diagramId, int hapticId, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart , int stipplePattern, double attractPointX, double attractPointY ) { + assert(xs.length == ys.length); + this.size = xs.length; + this.diagramId = diagramId; + this.hapticId = hapticId; + this.xs = xs; + this.ys = ys; + this.adjMatrix = adjMatrix; + this.stipplePattern = stipplePattern; + this.attractPointX = attractPointX; + this.attractPointY = attractPointY; + this.nodeStart = nodeStart; + } + + public double xs[] ; + public double ys[] ; + public int size; + public BitSet adjMatrix[]; + public int diagramId; + public int hapticId; + public int stipplePattern; + public double attractPointX; + public double attractPointY; + public int nodeStart; + + static final int DOTTED_LINE = 0xF0F0; + static final int DASHED_LINE = 0xAAAA; + static final int SOLID_LINE = 0xFFFF; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Empties.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,30 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.util.ArrayList; +import java.util.HashMap; + +class Empties { + public static final ArrayList<Node> EMPTY_NODE_LIST = new ArrayList<Node>(0); + public static final ArrayList<Edge> EMPTY_EDGE_LIST = new ArrayList<Edge>(0); + public static final HashMap<Integer,Node> EMPTY_NODE_MAP = new HashMap<Integer,Node>(); + public static final HashMap<Integer,Edge> EMPTY_EDGE_MAP = new HashMap<Integer,Edge>(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/FalconHaptics.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,304 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.geom.Line2D; +import java.io.File; +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.OsDetector; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; +import uk.ac.qmul.eecs.ccmi.utils.ResourceFileWriter; + +class FalconHaptics extends Thread implements Haptics { + static Haptics createInstance(HapticListenerThread listener) { + if(OsDetector.has64BitJVM()){// no 64 native library supported yet + return null; + } + + if(OsDetector.isWindows()){ + /* create a directory for the dll distributed with HAPI library */ + String libDir = PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")); + File hapiDir = new File(libDir,"HAPI"); + hapiDir.mkdir(); + + /* try to load .dll's. First copy it in the home/ccmi_editor_data/lib directory */ + String[] dlls = {"HAPI/pthreadVC2.dll","HAPI/FreeImage.dll","HAPI/freeglut.dll", + "HAPI/H3DUtil_vc9.dll","HAPI/HAPI_vc9.dll","FalconHaptics.dll"}; + ResourceFileWriter fileWriter = new ResourceFileWriter(); + for(String dll : dlls){ + URL url = OmniHaptics.class.getResource(dll); + fileWriter.setResource(url); + fileWriter.writeOnDisk(libDir, dll); + String path = fileWriter.getFilePath(); + try{ + if(path == null) + throw new UnsatisfiedLinkError(dll+" missing"); + System.load( path ); + }catch(UnsatisfiedLinkError e){ + System.err.println(e.getMessage()); + e.printStackTrace(); + return null; + } + } + }else{ + return null; + } + + FalconHaptics falcon = new FalconHaptics("FalconHaptics"); + falcon.hapticListener = listener; + /* start up the listener which immediately stops, waiting for commands */ + if(!falcon.hapticListener.isAlive()) + falcon.hapticListener.start(); + /* start up the haptics thread which issues commands from the java to the c++ thread */ + falcon.start(); + synchronized(falcon){ + try { + falcon.wait(); + }catch (InterruptedException ie) { + throw new RuntimeException(ie); // must never happen + } + } + if(falcon.initFailed) + return null; + else + return falcon; + } + + private FalconHaptics(String threadName){ + super(threadName); + + nodes = new HashMap<String,ArrayList<Node>>(); + edges = new HashMap<String,ArrayList<Edge>>(); + currentNodes = Empties.EMPTY_NODE_LIST; + currentEdges = Empties.EMPTY_EDGE_LIST; + + attractTo = 0; + } + + private native int initFalcon(int width, int height) throws IOException ; + + @Override + public void addNewDiagram(String diagramName) { + ArrayList<Node> cNodes = new ArrayList<Node>(30); + ArrayList<Edge> cEdges = new ArrayList<Edge>(30); + nodes.put(diagramName, cNodes); + edges.put(diagramName, cEdges); + + synchronized(this){ + currentNodes = cNodes; + currentEdges = cEdges; + collectionsChanged = true; + } + } + + @Override + public synchronized void switchDiagram(String diagramName) { + if(!nodes.containsKey(diagramName)) + throw new IllegalArgumentException("Diagram not found among added diagrams:" + diagramName); + + currentNodes = nodes.get(diagramName); + currentEdges = edges.get(diagramName); + collectionsChanged = true; + } + + @Override + public synchronized void removeDiagram(String diagramNameToRemove, + String diagramNameOfNext) { + if(!nodes.containsKey(diagramNameToRemove)) + throw new IllegalArgumentException("Diagram not found among added diagrams:" + diagramNameToRemove); + + nodes.remove(diagramNameToRemove); + edges.remove(diagramNameToRemove); + + if(diagramNameOfNext == null){ + currentNodes = Empties.EMPTY_NODE_LIST; + currentEdges = Empties.EMPTY_EDGE_LIST; + }else { + if(!nodes.containsKey(diagramNameOfNext)) + throw new IllegalArgumentException("Diagram not found among added diagrams:" + diagramNameOfNext); + currentNodes = nodes.get(diagramNameOfNext); + currentEdges = edges.get(diagramNameOfNext); + } + collectionsChanged = true; + } + + @Override + public synchronized void addNode(double x, double y, int nodeHashCode, String diagramName) { + Node n = new Node(x,y,nodeHashCode, nodeHashCode); + if(diagramName == null){ + currentNodes.add(n); + }else{ + nodes.get(diagramName).add(n); + } + collectionsChanged = true; + } + + @Override + public synchronized void removeNode(int nodeHashCode, String diagramName) { + ListIterator<Node> itr = (diagramName == null) ? currentNodes.listIterator() : nodes.get(diagramName).listIterator(); + while(itr.hasNext()){ + Node n = itr.next(); + if(n.diagramId == nodeHashCode){ + itr.remove(); + collectionsChanged = true; + break; + } + } + } + + @Override + public synchronized void moveNode(double x, double y, int nodeHashCode, + String diagramName) { + ArrayList<Node> iterationList = (diagramName == null) ? currentNodes : nodes.get(diagramName); + for(Node n : iterationList){ + if(n.diagramId == nodeHashCode){ + n.x = x; + n.y = y; + collectionsChanged = true; + break; + } + } + } + + @Override + public synchronized void addEdge(int edgeHashCode, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, int stipplePattern, + Line2D attractLine, String diagramName) { + /* 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,edgeHashCode, xs, ys, adjMatrix, nodeStart, stipplePattern, pX, pY); + /* add the edge reference to the edges list */ + if(diagramName == null){ + currentEdges.add(e); + }else{ + edges.get(diagramName).add(e); + } + collectionsChanged = true; + } + + @Override + public synchronized void updateEdge(int edgeHashCode, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, Line2D attractLine, + String diagramName) { + + 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; + } + } + collectionsChanged = true; + } + + @Override + public synchronized void removeEdge(int edgeHashCode, String diagramName) { + ListIterator<Edge> itr = (diagramName == null) ? currentEdges.listIterator() : edges.get(diagramName).listIterator(); + while(itr.hasNext()){ + Edge e = itr.next(); + if(e.diagramId == edgeHashCode){ + itr.remove(); + collectionsChanged = true; + break; + } + } + collectionsChanged = true; + } + + @Override + public synchronized void attractTo(int elementHashCode) { + attractTo = elementHashCode; + } + + @Override + public synchronized void pickUp(int elementHashCode) { + pickUp = true; + } + + @Override + public void setVisible(boolean visible) { + // falcon haptics window cannot be made invisible + } + + @Override + public synchronized void dispose(){ + shutdown = true; + /* wait for the haptic thread to shut down */ + try { + wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void run() { + /* 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(); + currentNodes.size(); + try { + initFalcon(screenWidth * 5 / 8,screenHeight * 5 / 8); + } catch (IOException e) { + throw new RuntimeException();// OMNI haptic device doesn't cause any exception + } + } + + /* the diagram currently selected */ + private ArrayList<Node> currentNodes; + private ArrayList<Edge> currentEdges; + + /* maps with all the diagrams in the editor */ + private HashMap<String,ArrayList<Node>> nodes; + private HashMap<String,ArrayList<Edge>> edges; + + private HapticListenerThread hapticListener; + private boolean initFailed; + private boolean collectionsChanged; + private boolean pickUp; + private int attractTo; + private boolean shutdown; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListSupport.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,129 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/* used by MouseHaptics */ +class HapticListSupport { + + public HapticListSupport(){ + nodes = new HashMap<String,List<Node>>(); + edges = new HashMap<String,List<Edge>>(); + currentNodes = Empties.EMPTY_NODE_LIST; + currentEdges = Empties.EMPTY_EDGE_LIST; + } + + public void addNewDiagram(String diagramName) { + currentNodes = new ArrayList<Node>(30); + nodes.put(diagramName,currentNodes); + currentEdges = new ArrayList<Edge>(30); + edges.put(diagramName,currentEdges); + } + + public void switchDiagram(String diagramName) { + currentNodes = nodes.get(diagramName); + currentEdges = edges.get(diagramName); + } + + public void removeDiagram(String diagramNameToRemove, + String diagramNameOfNext) { + nodes.remove(diagramNameToRemove); + edges.remove(diagramNameToRemove); + currentNodes = nodes.get(diagramNameOfNext); + currentEdges = edges.get(diagramNameOfNext); + if(currentNodes == null){ + currentNodes = Empties.EMPTY_NODE_LIST; + currentEdges = Empties.EMPTY_EDGE_LIST; + } + } + + public void addNode(Node n, String diagramName){ + if(diagramName == null) + currentNodes.add(n); + else + nodes.get(diagramName).add(n); + } + + public Node removeNode(int nodeHashCode, String diagramName) { + List<Node> list = (diagramName == null) ? currentNodes : nodes.get(diagramName); + for(Node n : list){ + if(n.diagramId == nodeHashCode){ + list.remove(n); + return n; + } + } + return null; + } + + public Node getNode(int nodeHashCode, String diagramName){ + List<Node> list = (diagramName == null) ? currentNodes : nodes.get(diagramName); + for(Node n : list){ + if(n.diagramId == nodeHashCode){ + return n; + } + } + return null; + } + + public void addEdge(Edge e, String diagramName){ + if(diagramName == null) + currentEdges.add(e); + else + edges.get(diagramName).add(e); + } + + public Edge removeEdge(int edgeHashCode, String diagramName){ + List<Edge> list = (diagramName == null) ? currentEdges : edges.get(diagramName); + for(Edge e : list){ + if(e.diagramId == edgeHashCode){ + list.remove(e); + return e; + } + } + return null; + } + + public Edge getEdge(int edgeHashCode, String diagramName){ + List<Edge> list = (diagramName == null) ? currentEdges : edges.get(diagramName); + for(Edge e : list){ + if(e.diagramId == edgeHashCode){ + return e; + } + } + return null; + } + + public List<Node> getCurrentNodes(){ + return currentNodes; + } + + public List<Edge> getCurrentEdges(){ + return currentEdges; + } + + private Map<String,List<Node>> nodes; + private Map<String,List<Edge>> edges; + private List<Node> currentNodes; + private List<Edge> currentEdges; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListener.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,27 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +/** + * An interface for executing commands sent from an haptic device. + */ +public interface HapticListener { + public void executeCommand(HapticListenerCommand cmd, int ID, double x, double y, double startX, double startY); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListenerCommand.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,72 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +/** + * enum of commands that an haptic listener can receive by an haptic device. + * + */ +public enum HapticListenerCommand { + PLAY_ELEMENT_SOUND, + PLAY_ELEMENT_SPEECH, + PLAY_SOUND, + SELECT, + UNSELECT, + MOVE, + INFO, + NONE, + PICK_UP, + ERROR; + + public static HapticListenerCommand fromChar(char c){ + switch(c){ + case 'p' : return PLAY_ELEMENT_SOUND; + case 't' : return PLAY_ELEMENT_SPEECH; + case 's' : return SELECT; + case 'm' : return MOVE; + case 'i' : return INFO; + case 'u' : return UNSELECT; + case 'g' : return PLAY_SOUND; + case 'e' : return ERROR; + case 'c' : return PICK_UP; + default : return NONE; + } + } + + public enum Sound { + NONE, + MAGNET_OFF, + MAGNET_ON, + HOOK_ON, + DRAG; + + public static Sound fromInt(int i){ + switch(i){ + case 0 : return MAGNET_OFF; + case 1 : return MAGNET_ON; + case 3 : return DRAG; + default : return NONE; + } + + } + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticListenerThread.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,72 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +/** + * + * An HapticListenerThread is a thread listening to commands sent by an haptic device through + * shared memory and executes them. The piece of software that manages the haptic device + * runs on its own thread, hence the need for a inter-thread communication. Listening + * to the haptic device cannot be done by the event dispatching thread as this + * would prevent the user from using the graphical user interface, therefore a further thread is needed + * for this task. + * HapticListener is an abstract class which must be extended by implementing the + * {@link #executeCommand(HapticListenerCommand, int, double, double, double, double)} method. + * + */ +public abstract class HapticListenerThread extends Thread implements HapticListener { + + public HapticListenerThread() { + super("Haptic Listener"); + mustSayGoodBye = false; + } + + @Override + public final void run(){ + synchronized(this){ + while(!mustSayGoodBye){ + try { + wait(); + executeCommand(HapticListenerCommand.fromChar(cmd), diagramElementID, x, y, startX, startY); + notify(); // notify the command has been executed + } catch (InterruptedException e) { + dispose(); + } + } + } + } + + @Override + public abstract void executeCommand(HapticListenerCommand cmd, int ID, double x, double y, double startX, double startY); + + public abstract HapticListener getNonRunnableListener(); + + public void dispose(){ + mustSayGoodBye = true; + } + + private char cmd; + private int diagramElementID; + private boolean mustSayGoodBye; + private double x; + private double y; + private double startX; + private double startY; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Haptics.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,70 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.awt.geom.Line2D; +import java.util.BitSet; + +/** + * + * An interface for rendering a visual diagram hapticly. + * + */ +public interface Haptics extends Runnable{ + + public void addNewDiagram(String diagramName); + + public void switchDiagram(String diagramName); + + /** + * Removes a diagram from the collection of haptic diagrams. + * + * @param diagramNameToRemove the unique name of the diagram to remove + * @param diagramNameOfNext the name of the next diagram to render hapticly. If + * {@code null}, no diagram will be rendered. + */ + public void removeDiagram(String diagramNameToRemove, String diagramNameOfNext); + + public void addNode(double x, double y, int nodeHashCode, String diagramName); + + public void removeNode(int nodeHashCode, String diagramName); + + public void moveNode(double x, double y, int nodeHashCode, String diagramName); + + public void addEdge(int edgeHashCode, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, int stipplePattern, + Line2D attractLine, String diagramName); + + public void updateEdge(int edgeHashCode, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, Line2D attractLine, String diagramName); + + public void removeEdge(int edgeHashCode, String diagramName); + + public void attractTo(int elementHashCode); + + public void pickUp(int elementHashCode); + + public boolean isAlive(); + + public void setVisible(boolean visible); + + public void dispose(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/HapticsFactory.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,90 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/** + * + * Creates an instance of a class implementing the Haptics interface. There can only be one instance of such + * class. Therefore the factory uses the singleton pattern to always return the same object + * after it is created. + * + */ +public class HapticsFactory { + /** + * 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(HapticListenerThread listener) { + if(hapticsInstance != null) + throw new IllegalStateException("create instance must be called once only"); + + /* use the preferences service to pick the right device to use (the one the user selected * + * in their preferences). This allows to avoid loading the .dll file for the other device * + * that the user doesn't need, which would entail a waste of memory and, more important * + * a conflict between function names. */ + String defaultDevice = PreferencesService.getInstance().get("haptic_device", TABLET_ID); + + /* temporary set the preference to default: if the device crashes during init * + * the user will still be able to restart the diagram editor */ + PreferencesService.getInstance().put("haptic_device", TABLET_ID); + + if(PHANTOM_ID.equals(defaultDevice) || true){//FIXME + /* OmniHaptics first */ + hapticsInstance = OmniHaptics.createInstance(listener); + if(hapticsInstance != null){//OmniHaptics instance successfully created: return + PreferencesService.getInstance().put("haptic_device", defaultDevice); + return; + } + }else if(FALCON_ID.equals(defaultDevice)){ + /* Falcon first */ + hapticsInstance = FalconHaptics.createInstance(listener); + if(hapticsInstance != null){ //FalconHaptics instance successfully created: return + PreferencesService.getInstance().put("haptic_device", defaultDevice); + return; + } + } + + PreferencesService.getInstance().put("haptic_device", defaultDevice); + /* no devices available, stop the listener and go for the default */ + if(listener.isAlive()){ + listener.interrupt(); + } + hapticsInstance = MouseHaptics.createInstance(listener.getNonRunnableListener()); + if(hapticsInstance == null) + throw new RuntimeException(); + + } + + public static Haptics getInstance(){ + if(hapticsInstance == null){ + throw new IllegalStateException("static method createInstance() must be called before getInstance()"); + } + return hapticsInstance; + } + + private static Haptics hapticsInstance; + public static final String PHANTOM_ID = "Phantom Omni"; + public static final String FALCON_ID = "Falcon"; + public static final String TABLET_ID = "Tablet/Mouse"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/MouseHaptics.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,452 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.awt.AWTException; +import java.awt.BasicStroke; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Robot; +import java.awt.Stroke; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowFocusListener; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.io.IOException; +import java.util.BitSet; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import uk.ac.qmul.eecs.ccmi.gui.DiagramPanel; +import uk.ac.qmul.eecs.ccmi.main.DiagramEditorApp; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; + +@SuppressWarnings("serial") +class MouseHaptics extends JPanel implements Haptics, KeyListener, MouseListener, MouseMotionListener { + static Haptics createInstance(HapticListener listener){ + MouseHaptics instance = new MouseHaptics(listener); + try{ + instance.initMouseHaptics(); + return instance; + }catch(IOException ioe){ + return null; + } + } + + private MouseHaptics(HapticListener listener){ + this.listener = listener; + solidStroke = new BasicStroke(EDGE_THICKNESS); + dottedStroke = new BasicStroke(EDGE_THICKNESS, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 0.0f, + new float[]{1.0f,30.0f}, + 0.0f); + dashedStroke = new BasicStroke(EDGE_THICKNESS, + BasicStroke.CAP_ROUND, + BasicStroke.JOIN_ROUND, + 0.0f, + new float[]{80.0f,50.0f}, + 0.0f); + } + + @Override + public void paintComponent(Graphics g){ + super.paintComponent(g); + + Graphics2D g2 = (Graphics2D)g; + + Stroke oldStroke = g2.getStroke(); + // draw edges + g2.setColor(EDGE_COLOR); + for(Edge e : listSupport.getCurrentEdges()){ + switch(e.stipplePattern){ + case Edge.SOLID_LINE : + g2.setStroke(solidStroke);break; + case Edge.DOTTED_LINE : + g2.setStroke(dottedStroke);break; + case Edge.DASHED_LINE : + g2.setStroke(dashedStroke);break; + } + for(int i=0; i< e.adjMatrix.length; i++){ + BitSet adj = e.adjMatrix[i]; + for (int j = adj.nextSetBit(0); j >= 0; j = adj.nextSetBit(j+1)) { + g2.drawLine((int)e.xs[i], (int)e.ys[i], (int)e.xs[j], (int)e.ys[j]); + } + } + } + + g2.setStroke(oldStroke); + // draw nodes + g2.setColor(NODE_COLOR); + for(Node n : listSupport.getCurrentNodes()){ + g2.fillOval((int)(n.x - NODE_DIAMETER/2), (int)(n.y - NODE_DIAMETER/2), NODE_DIAMETER, NODE_DIAMETER); + } + } + + @Override + public void run() {} + + void initMouseHaptics() throws IOException { + hapticFrame = new JFrame("Haptic Frame"); + try { + robot = new Robot(); + } catch (AWTException e) { + throw new IOException(e); + } + /* this is necessary to make the window screen size. it doens't work with the default layout */ + setLayout(new BorderLayout()); + + setBackground(Color.black); + + listSupport = new HapticListSupport(); + + hapticFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + hapticFrame.addWindowFocusListener(new WindowFocusListener(){ + @Override + public void windowGainedFocus(WindowEvent arg0) { + NarratorFactory.getInstance().speak("Haptic window Focused"); + } + + @Override + public void windowLostFocus(WindowEvent arg0) {} + }); + hapticFrame.setContentPane(this); + hapticFrame.setExtendedState(JFrame.MAXIMIZED_BOTH); + hapticFrame.setUndecorated(true); + hapticFrame.addKeyListener(this); + addMouseMotionListener(this); + addMouseListener(this); + hapticFrame.pack(); + } + + @Override + public void addNewDiagram(String diagramName) { + listSupport.addNewDiagram(diagramName); + repaint(); + } + + @Override + public void switchDiagram(String diagramName) { + listSupport.switchDiagram(diagramName); + repaint(); + } + + @Override + public void removeDiagram(String diagramNameToRemove, + String diagramNameOfNext) { + listSupport.removeDiagram(diagramNameToRemove, diagramNameOfNext); + repaint(); + } + + @Override + public void addNode(double x, double y, int nodeHashCode, String diagramName) { + Node n = new Node(x,y,nodeHashCode, 0); + listSupport.addNode(n,diagramName); + repaint(); + } + + @Override + public void removeNode(int nodeHashCode, String diagramName) { + listSupport.removeNode(nodeHashCode, diagramName); + repaint(); + } + + @Override + public void moveNode(double x, double y, int nodeHashCode, + String diagramName) { + Node n = listSupport.getNode(nodeHashCode, diagramName); + n.x = x; + n.y = y; + repaint(); + } + + @Override + public void addEdge(int edgeHashCode, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, int stipplePattern, + Line2D attractLine, String diagramName) { + // 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,0, xs, ys, + adjMatrix, nodeStart, stipplePattern, + pX, pY); + listSupport.addEdge(e, diagramName); + repaint(); + } + + @Override + public void updateEdge(int edgeHashCode, double[] xs, double[] ys, + BitSet[] adjMatrix, int nodeStart, Line2D attractLine, + String diagramName) { + Edge e = listSupport.getEdge(edgeHashCode, diagramName); + e.xs = xs; + e.ys = ys; + e.adjMatrix = adjMatrix; + e.nodeStart = nodeStart; + repaint(); + } + + @Override + public void removeEdge(int edgeHashCode, String diagramName) { + listSupport.removeEdge(edgeHashCode, diagramName); + repaint(); + } + + @Override + public void attractTo(int elementHashCode) { + + } + + @Override + public void pickUp(int elementHashCode) { + // needed to change the status of the haptic device. not needed here + } + + @Override + public boolean isAlive() { + return false; + } + + @Override + public void setVisible(boolean visible){ + hapticFrame.setVisible(visible); + } + + @Override + public void dispose() { + // no resources to free up + } + + @Override + public void keyPressed(KeyEvent evt) { + DiagramPanel panel = DiagramEditorApp.getFrame().getActiveTab(); + if(panel == null) + DiagramEditorApp.getFrame().editorTabbedPane.dispatchEvent(evt); + else + panel.getTree().dispatchEvent(evt); + } + + @Override + public void keyReleased(KeyEvent evt) { + keyPressed(evt); + } + + @Override + public void keyTyped(KeyEvent evt) { + keyPressed(evt); + } + + @Override + public void mouseDragged(MouseEvent evt) { + /* priority to nodes: check if the mouse pointer is inside a circle centred on * + * the node and with radius equal to NODE_RADIUS. If the mouse pointer is within * + * the radius of two or more nodes then the closest is picked up. The command is * + * executed once when the node is touched. In order have it executed again the * + * used must get away from it and hover above it again */ + Point2D p = evt.getPoint(); + if(elementPickedUp){ + draggedDistance += evt.getPoint().distance(lastDragPoint); + if( draggedDistance > CHAIN_SOUND_INTERVAL){ + listener.executeCommand(HapticListenerCommand.PLAY_SOUND, 3, 0,0,0,0); + draggedDistance = 0; + } + lastDragPoint = evt.getPoint(); + } + + Node hoveredNode = null; + for(Node n : listSupport.getCurrentNodes()){ + double distance = p.distance(n.x, n.y); + if( distance < NODE_HOVER_DIST){ + if(hoveredNode == null || distance < p.distance(hoveredNode.x,hoveredNode.y) ){ + hoveredNode = n; + } + } + } + if(hoveredNode != null && hoveredNode != lastTouchedNode){ + listener.executeCommand(HapticListenerCommand.PLAY_ELEMENT_SPEECH, hoveredNode.diagramId, 0, 0, 0, 0); + lastTouchedNode = hoveredNode; + lastTouchedEdge = null; + return; + } + lastTouchedNode = hoveredNode; + /* if hovering inside a node neither send the command nor take edges into account */ + if(hoveredNode != null) + return; + + /* if no node is being touched, check the edges out. */ + Edge hoveredEdge = null; + Line2D line = new Line2D.Double(); + /* look at all edges */ + for(Edge e : listSupport.getCurrentEdges()){ + /* look at all edge's lines */ + for(int i=0; i< e.adjMatrix.length; i++){ + BitSet adj = e.adjMatrix[i]; + for (int j = adj.nextSetBit(0); j >= 0; j = adj.nextSetBit(j+1)) { + line.setLine(e.xs[i], e.ys[i], e.xs[j], e.ys[j]); + if(lastTouchedEdge != e && line.ptSegDist(p)<EDGE_HOVER_DIST){ + hoveredEdge = e; + listener.executeCommand(HapticListenerCommand.PLAY_ELEMENT_SPEECH, hoveredEdge.diagramId, 0, 0, 0, 0); + lastTouchedEdge = hoveredEdge; + return; + } + } + } + } + lastTouchedEdge = hoveredEdge; + } + + @Override + public void mouseMoved(MouseEvent evt) { + /* right click on a graphic tablet used as mouse has the effect of nullifying the * + * dragging. That is there is no right click but rather it's like if you untouch * + * the tablet. In order to address this a robot is used in order to re-leftclick each time * + * the right click is pressed. In this way we assure that the left click is always held * + * and therefore the mouse is always dragging rather than moving */ + if(mustReclick){ + mustReclick = false; + reclickedAfterMove = true; // this is to make this.mousePressed() have no effect, when it's the robot clicking + robot.mousePress(InputEvent.BUTTON1_MASK); + } + } + + @Override + public void mousePressed(MouseEvent evt) { + /* by clicking on the object, its name is stated by the TTS. * + * Much as what happens when hovering on it with the button pressed */ + if(evt.getButton() == MouseEvent.BUTTON1){ + if(reclickedAfterMove){ + reclickedAfterMove = false; + return; + } + lastTouchedEdge = null; // these two fields are used with dragging to avoid repeating + lastTouchedNode = null; + + mouseDragged(evt);//left clicking on an object is like to drag on it + return; + } + /* button 3 (right click) is for moving the objects (picking up and dropping) */ + if(evt.getButton() != MouseEvent.BUTTON3) + return; + if(!secondClick){ // clicked for the first time: pick up the node or edge + Point2D p = evt.getPoint(); + Node hoveredNode = null; + for(Node n : listSupport.getCurrentNodes()){ + double distance = p.distance(n.x, n.y); + if( distance < NODE_HOVER_DIST){ + if(hoveredNode == null || distance < p.distance(hoveredNode.x,hoveredNode.y) ){ + hoveredNode = n; + } + } + } + if(hoveredNode != null){ // clicked on a node + listener.executeCommand(HapticListenerCommand.PICK_UP, hoveredNode.diagramId, 0, 0, 0, 0); + secondClick = true; + startX = evt.getX(); + startY = evt.getY(); + pickedUpElementId = hoveredNode.diagramId; + mustReclick = true; + /* sets the variables for the chain sound when dragging the element around */ + elementPickedUp = true; + lastDragPoint = evt.getPoint(); + return; + } + /* if no node is being touched, check the edges out. */ + Line2D line = new Line2D.Double(); + /* look at all edges */ + for(Edge e : listSupport.getCurrentEdges()){ + /* look at all edge's lines */ + for(int i=0; i< e.adjMatrix.length; i++){ + BitSet adj = e.adjMatrix[i]; + for (int j = adj.nextSetBit(0); j >= 0; j = adj.nextSetBit(j+1)) { + line.setLine(e.xs[i], e.ys[i], e.xs[j], e.ys[j]); + if(/*lastTouchedEdge != e && */line.ptSegDist(p)<EDGE_HOVER_DIST){ + listener.executeCommand(HapticListenerCommand.PICK_UP, e.diagramId, 0, 0, 0, 0); + secondClick = true; + startX = evt.getX(); + startY = evt.getY(); + pickedUpElementId = e.diagramId; + mustReclick = true; + /* sets the variables for the chain sound when dragging the element around */ + elementPickedUp = true; + lastDragPoint = evt.getPoint(); + return; + } + } + } + } + }else{ + listener.executeCommand(HapticListenerCommand.MOVE, pickedUpElementId, evt.getX(), evt.getY(), startX, startY); + elementPickedUp = false; + secondClick = false; + } + mustReclick = true; + } + + @Override + public void mouseEntered(MouseEvent evt) {} + + @Override + public void mouseExited(MouseEvent evt) {} + + @Override + public void mouseClicked(MouseEvent evt) {} + + @Override + public void mouseReleased(MouseEvent evt) {} + + private HapticListener listener; + private JFrame hapticFrame; + private HapticListSupport listSupport; + private Stroke solidStroke; + private Stroke dashedStroke; + private Stroke dottedStroke; + private Node lastTouchedNode; + private Edge lastTouchedEdge; + private Robot robot; + private boolean mustReclick; + private boolean reclickedAfterMove; + private boolean secondClick; + private boolean elementPickedUp; + private Point2D lastDragPoint; + private double draggedDistance; + private int startX; + private int startY; + private int pickedUpElementId; + private static final Color EDGE_COLOR = Color.RED; + private static final Color NODE_COLOR = Color.WHITE; + private static final int EDGE_THICKNESS = 26;//2; + private static final int NODE_DIAMETER = 50;//10; + private static final int NODE_HOVER_DIST = 25; + private static final int EDGE_HOVER_DIST = 13; + private static final double CHAIN_SOUND_INTERVAL = 150; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/Node.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,45 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.util.ArrayList; + +/* + * + * A diagram node in the haptics space. + * + */ +class Node { + public Node(double x, double y, int diagramId, int hapticId){ + this.x = x; + this.y = y; + this.diagramId = diagramId; + this.hapticId = hapticId; + edges = new ArrayList<Edge>(5); + } + + + public double x; + public double y; + /* the id on the diagram "id space". it corresponds to the hash code of the diagram nodes */ + public int diagramId; // not shared with the haptic thread + public int hapticId; + public ArrayList<Edge> edges; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/OmniHaptics.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,434 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.haptics; + +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.geom.Line2D; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; +import java.util.ListIterator; + +import uk.ac.qmul.eecs.ccmi.utils.ResourceFileWriter; +import uk.ac.qmul.eecs.ccmi.utils.OsDetector; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/* + * + * The implementation of the Haptics interface which uses Sensable® + * PHANTOM Omni® haptic device. + * + */ +class OmniHaptics extends Thread implements Haptics { + + static Haptics createInstance(HapticListenerThread listener) { + if(listener == null) + throw new IllegalArgumentException("listener cannot be null"); + + if(OsDetector.has64BitJVM()){ + return null;// no 64 native library supported yet + } + + if(OsDetector.isWindows()){ + /* try to load .dll. First copy it in the home/ccmi_editor_data/lib directory */ + URL url = OmniHaptics.class.getResource("OmniHaptics.dll"); + ResourceFileWriter fileWriter = new ResourceFileWriter(url); + fileWriter.writeOnDisk( + PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")), + "OmniHaptics.dll"); + String path = fileWriter.getFilePath(); + try{ + if(path == null) + throw new UnsatisfiedLinkError("OmniHaptics.dll missing"); + System.load( path ); + }catch(UnsatisfiedLinkError e){ + e.printStackTrace(); + 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 */ + /* don't kill the listener as initialization will be tried on other devices */ +// while(!omniHaptics.hapticListener.isInterrupted()){ +// omniHaptics.hapticListener.interrupt(); +// } +// omniHaptics.hapticListener = null; //leave the listener to the GC + return null; + }else{ + return omniHaptics; + } + } + + private OmniHaptics(String threadName){ + super(threadName); + /* get the screen size which will be passed to init methos in order to set up a window + * for the haptic with the same size as the swing one + */ + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + + int screenWidth = (int)screenSize.getWidth(); + int screenHeight = (int)screenSize.getHeight(); + + width = screenWidth * 5 / 8; + height = screenHeight * 5 / 8; + + newHapticId = false; + dumpHapticId = false; + shutdown = false; + hapticInitFailed = false; + nodes = new HashMap<String,ArrayList<Node>>(); + edges = new HashMap<String,ArrayList<Edge>>(); + currentNodes = Empties.EMPTY_NODE_LIST; + currentEdges = Empties.EMPTY_EDGE_LIST; + nodesMaps = new HashMap<String,HashMap<Integer,Node>>(); + edgesMaps = new HashMap<String,HashMap<Integer,Edge>>(); + currentNodesMap = Empties.EMPTY_NODE_MAP; + currentEdgesMap = Empties.EMPTY_EDGE_MAP; + } + + + public native int initOmni(int width, int height) throws IOException; + + @Override + public void addNewDiagram(String diagramName){ + ArrayList<Node> cNodes = new ArrayList<Node>(30); + ArrayList<Edge> cEdges = new ArrayList<Edge>(30); + nodes.put(diagramName, cNodes); + edges.put(diagramName, cEdges); + + HashMap<Integer,Node> cNodesMap = new HashMap<Integer,Node>(); + HashMap<Integer,Edge> cEdgesMap = new HashMap<Integer,Edge>(); + nodesMaps.put(diagramName, cNodesMap); + edgesMaps.put(diagramName, cEdgesMap); + + 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 not found among added diagrams:" + diagramName); + + 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("Diagram not found among added diagrams:" + diagramNameToRemove); + + nodes.remove(diagramNameToRemove); + edges.remove(diagramNameToRemove); + nodesMaps.remove(diagramNameToRemove); + edgesMaps.remove(diagramNameToRemove); + if(diagramNameNext == null){ + currentNodes = Empties.EMPTY_NODE_LIST; + currentEdges = Empties.EMPTY_EDGE_LIST; + currentNodesMap = Empties.EMPTY_NODE_MAP; + currentEdgesMap = Empties.EMPTY_EDGE_MAP; + }else{ + if(!nodes.containsKey(diagramNameNext)) + throw new IllegalArgumentException("Diagram not found among added diagrams:" + diagramNameNext); + currentNodes = nodes.get(diagramNameNext); + currentEdges = edges.get(diagramNameNext); + currentNodesMap = nodesMaps.get(diagramNameNext); + currentEdgesMap = edgesMaps.get(diagramNameNext); + } + } + + @Override + public synchronized void addNode(double x, double y, int nodeHashCode, String diagramName){ + newHapticId = true; + // waits for an identifier from the openGL thread + try{ + wait(); + }catch(InterruptedException ie){ + wasInterrupted(); + } + + Node n = new Node(x,y,nodeHashCode, currentHapticId); + if(diagramName == null){ + currentNodes.add(n); + currentNodesMap.put(currentHapticId, n); + }else{ + nodes.get(diagramName).add(n); + nodesMaps.get(diagramName).put(currentHapticId, n); + } + } + + @Override + public synchronized void removeNode(int nodeHashCode, String diagramName){ + ListIterator<Node> itr = (diagramName == null) ? currentNodes.listIterator() : nodes.get(diagramName).listIterator(); + boolean found = false; + int hID = -1; + while(itr.hasNext()){ + Node n = itr.next(); + if(n.diagramId == nodeHashCode){ + hID = n.hapticId; + itr.remove(); + found = true; + break; + } + } + assert(found); + + /* remove the node from the map as well */ + if(diagramName == null) + currentNodesMap.remove(hID); + else + nodesMaps.get(diagramName).remove(hID); + + /* set the flag to ask the haptic thread to free the id of the node that's been deleted */ + dumpHapticId = true; + /* share the id to free with the other thread */ + currentHapticId = hID; + try{ + wait(); + }catch(InterruptedException ie){ + wasInterrupted(); + } + } + + @Override + public synchronized void moveNode(double x, double y, int nodeHashCode, String diagramName){ + ArrayList<Node> iterationList = (diagramName == null) ? currentNodes : nodes.get(diagramName); + for(Node n : iterationList){ + if(n.diagramId == nodeHashCode){ + n.x = x; + n.y = y; + break; + } + } + } + + @Override + public synchronized void addEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart, int stipplePattern, Line2D attractLine, String diagramName){ + // flag the openGL thread the fact we need an identifier + newHapticId = true; + // waits for an identifier from the openGL thread + try{ + wait(); + }catch(InterruptedException ie){ + wasInterrupted(); + } + + // find the mid point of the line of attraction + double pX = Math.min(attractLine.getX1(), attractLine.getX2()); + double pY = Math.min(attractLine.getY1(), attractLine.getY2()); + pX += Math.abs(attractLine.getX1() - attractLine.getX2())/2; + pY += Math.abs(attractLine.getY1() - attractLine.getY2())/2; + + Edge e = new Edge(edgeHashCode,currentHapticId, xs, ys, adjMatrix, nodeStart, stipplePattern, pX, pY); + if(diagramName == null){ + /* add the edge reference to the Haptic edges list */ + currentEdges.add(e); + /* add the edge reference to the Haptic edges map */ + currentEdgesMap.put(currentHapticId, e); + }else{ + /* add the edge reference to the Haptic edges list */ + edges.get(diagramName).add(e); + /* add the edge reference to the Haptic edges map */ + edgesMaps.get(diagramName).put(currentHapticId, e); + } + } + + @Override + public synchronized void updateEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart, Line2D attractLine, String diagramName){ + assert(xs.length == ys.length); + + for(Edge e : currentEdges){ + if(e.diagramId == edgeHashCode){ + e.xs = xs; + e.ys = ys; + e.size = xs.length; + e.adjMatrix = adjMatrix; + e.nodeStart = nodeStart; + // find the mid point of the line of attraction + double pX = Math.min(attractLine.getX1(), attractLine.getX2()); + double pY = Math.min(attractLine.getY1(), attractLine.getY2()); + pX += Math.abs(attractLine.getX1() - attractLine.getX2())/2; + pY += Math.abs(attractLine.getY1() - attractLine.getY2())/2; + e.attractPointX = pX; + e.attractPointY = pY; + } + } + } + + @Override + public synchronized void removeEdge(int edgeHashCode, String diagramName){ + ListIterator<Edge> itr = (diagramName == null) ? currentEdges.listIterator() : edges.get(diagramName).listIterator(); + boolean found = false; + int hID = -1; + while(itr.hasNext()){ + Edge e = itr.next(); + if(e.diagramId == edgeHashCode){ + hID = e.hapticId; + itr.remove(); + found = true; + break; + } + } + assert(found); + + /* remove the edge from the map as well */ + if(diagramName == null) + currentEdgesMap.remove(hID); + else + edgesMaps.get(diagramName).remove(hID); + /* set the flag to ask the haptic thread to free the id of the node that's been deleted */ + dumpHapticId = true; + /* share the id to free with the other thread */ + currentHapticId = hID; + try{ + wait(); + }catch(InterruptedException ie){ + wasInterrupted(); + } + } + + @Override + public synchronized void attractTo(int elementHashCode){ + attractToHapticId = findElementHapticID(elementHashCode); + attractTo = true; + } + + @Override + public synchronized void pickUp(int elementHashCode){ + pickUpHapticId = findElementHapticID(elementHashCode); + pickUp = true; + } + + private int findElementHapticID(int elementHashCode){ + int hID = -1; + boolean found = false; + for(Node n : currentNodes){ + if(n.diagramId == elementHashCode){ + hID = n.hapticId; + found = true; + break; + } + } + + if(!found) + for(Edge e : currentEdges){ + if(e.diagramId == elementHashCode){ + hID = e.hapticId; + found = true; + break; + } + } + assert(found); + return hID; + } + + @Override + public void setVisible(boolean visible){ + // not implemented but required by Haptic interface + } + + @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 { + initOmni(width,height); + } catch (IOException e) { + throw new RuntimeException();// OMNI haptic device doesn't cause any exception + } + } + + private void wasInterrupted(){ + throw new UnsupportedOperationException("Haptics thread interrupted and no catch block implemented"); + } + + private Node getNodeFromID(int hID){ + return currentNodesMap.get(hID); + } + + private Edge getEdgeFromID(int hID){ + return currentEdgesMap.get(hID); + } + + /* the diagram currently selected */ + private ArrayList<Node> currentNodes; + private ArrayList<Edge> currentEdges; + private HashMap<Integer,Node> currentNodesMap; + private HashMap<Integer,Edge> currentEdgesMap; + + /* maps with all the diagrams in the editor */ + private HashMap<String,ArrayList<Node>> nodes; + private HashMap<String,ArrayList<Edge>> edges; + private HashMap<String,HashMap<Integer,Node>> nodesMaps; + private HashMap<String,HashMap<Integer,Edge>> edgesMaps; + private int width; + private int height; + private int attractToHapticId; + private int pickUpHapticId; + /* flag for synchronization with the haptic thread*/ + private boolean newHapticId; + private boolean dumpHapticId; + private boolean shutdown; + boolean hapticInitFailed; + private boolean attractTo; + private boolean pickUp; + /* currentHapticId is used to share haptic ids between the threads */ + private int currentHapticId; + private HapticListenerThread hapticListener; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,297 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.main; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +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; +import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleTemplateEditor; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.CCmIUncaughtExceptionHandler; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; +import uk.ac.qmul.eecs.ccmi.utils.ResourceFileWriter; + +/** + * + * The application class with the main method. The main performs + * the start up initialization and then displays the graphical user interface. + * + */ +public class DiagramEditorApp implements Runnable { + + /* initialize all the non gui resources */ + private void init() throws IOException { + Thread.setDefaultUncaughtExceptionHandler(new CCmIUncaughtExceptionHandler()); + final ResourceBundle resources = ResourceBundle.getBundle(DiagramEditorApp.class.getName()); + /* create the home directory if it does not exist and store the path into the preferences */ + PreferencesService preferences = PreferencesService.getInstance(); + String homeDirPath = preferences.get("home", null); + if(homeDirPath == null){ + homeDirPath = new StringBuilder(System.getProperty("user.home")).append(System.getProperty("file.separator")).append(resources.getString("dir.home")).toString(); + preferences.put("home", homeDirPath); + } + File homeDir = new File(homeDirPath); + mkDir(homeDir, resources); + + File backupDir = new File(homeDir,resources.getString("dir.backups")); + mkDir(backupDir, resources); + backupDirPath = backupDir.getAbsolutePath(); + + /* create the templates directory into the home directory */ + File templateDir = new File(homeDir,resources.getString("dir.templates")); + mkDir(templateDir,resources); + + /* create the images directory into the home directory */ + File imagesDir = new File(homeDir,resources.getString("dir.images")); + 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")); + 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")); + 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.writeOnDisk(templateDir.getAbsolutePath(),"UML Diagram.xml"); + resourceWriter.setResource(getClass().getResource("Tube.xml")); + resourceWriter.writeOnDisk(templateDir.getAbsolutePath(),"Tube.xml"); + resourceWriter.setResource(getClass().getResource("Organization Chart.xml")); + resourceWriter.writeOnDisk(templateDir.getAbsolutePath(),"Organization Chart.xml"); + + /* read the template files into an array to pass to the EditorFrame instance */ + FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(File f, String name) { + return (name.endsWith(resources.getString("template.extension"))); + } + }; + templateFiles = templateDir.listFiles(filter); + File logDir = new File(homeDir,resources.getString("dir.log")); + mkDir(logDir,resources); + preferences.put("dir.log", logDir.getAbsolutePath()); + + String enableLog = preferences.get("enable_log", "false"); + if(Boolean.parseBoolean(enableLog)){ + try{ + InteractionLog.enable(logDir.getAbsolutePath()); + InteractionLog.log("PROGRAM STARTED"); + }catch(IOException ioe){ + /* if logging was enabled, the possibility to log is considered inescapable */ + /* at launch time: do not allow the execution to continue any further */ + throw new RuntimeException(ioe); + } + } + + /* create sound, speech engines */ + NarratorFactory.createInstance(); + SoundFactory.createInstance(); + } + + /* loads haptic device. If the user is running the software for the first time * + * they're prompted with a dialog to select the device they want to use */ + private void initHaptics(){ + final PreferencesService preferences = PreferencesService.getInstance(); + if(!Boolean.parseBoolean(preferences.get("haptic_device.initialized", "false"))){ + try { + SwingUtilities.invokeAndWait(new Runnable(){ + @Override + public void run() { + try { + UIManager.setLookAndFeel( + UIManager.getSystemLookAndFeelClassName()); + }catch(Exception e){/* nevermind */} + + String [] hapticDevices = { + HapticsFactory.PHANTOM_ID, + HapticsFactory.FALCON_ID, + HapticsFactory.TABLET_ID}; + String selection = (String)SpeechOptionPane.showSelectionDialog(null, + ResourceBundle.getBundle(DiagramEditorApp.class.getName()).getString("haptics_init.welcome"), + hapticDevices, + hapticDevices[2]); + if(selection == null) + System.exit(0); + preferences.put("haptic_device", selection); + preferences.put("haptic_device.initialized", "true"); + } + }); + }catch(InvocationTargetException ite){ + throw new RuntimeException(ite); + }catch(InterruptedException ie){ + throw new RuntimeException(ie); + } + } + + /* creates the Haptics instance */ + HapticsFactory.createInstance(new HapticKindle()); + haptics = HapticsFactory.getInstance(); + if(haptics.isAlive()) + NarratorFactory.getInstance().speakWholeText("Haptic device successfully initialized"); + } + + /* return true if the directory was created, false if it existed before */ + 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; + } + + /** + * build up the GUI and display it + */ + @Override + public void run() { + editorFrame = new EditorFrame(haptics,getTemplateFiles(),backupDirPath,getTemplateEditors(),getDiagrams()); + } + + /** + * Provides template editors to create own templates using the diagram editor. + * + * Subclasses who don't want any template editor to appear in the diagram + * can just return an empty array. Returning {@code null} will throw an exception. + * + * @return an array of template editors + */ + protected TemplateEditor[] getTemplateEditors(){ + TemplateEditor[] templateEditors = new TemplateEditor[1]; + templateEditors[0] = new SimpleTemplateEditor(); + return templateEditors; + } + + /** + * Returns the template files detected in the ccmi_editor_data/templates directory. + * + * Returning {@code null} will throw an exception. + * + * @return an array of (xml) Files containing a template + */ + protected File[] getTemplateFiles(){ + return templateFiles; + } + + /** + * Returns an empty list. This method can be overwritten by subclasses to + * provide their own custom diagrams. Such diagrams will appear in the menu. + * + * Returning {@code null} will throw an exception. + * + * @return an array of diagram templates. The array is empty in this implementation. + */ + protected Diagram[] getDiagrams(){ + return new Diagram[0]; + } + + /** + * The main function + * @param args this software accepts no args from the command line + */ + public static void main(String[] args){ + DiagramEditorApp application = new DiagramEditorApp(); + mainImplementation(application); + } + + + /** + * Implementation of the main body. It can be used to run the program + * using a subclass of {@code DiagramEditorApp}, providing it's own + * diagram templates + * + * @param application the diagram editor application to execute + */ + public final static void mainImplementation(DiagramEditorApp application) { + try{ + application.init(); + } catch (IOException e) { + final String msg = e.getLocalizedMessage(); + try { + SwingUtilities.invokeAndWait(new Runnable(){ + @Override + public void run(){ + try { + UIManager.setLookAndFeel( + UIManager.getSystemLookAndFeelClassName()); + }catch(Exception e){/* nevermind */} + JOptionPane.showMessageDialog(null, msg); + } + }); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } catch (InvocationTargetException ex) { + throw new RuntimeException(ex); + } + System.exit(-1); + } + + application.initHaptics(); + + /* start the application */ + try { + SwingUtilities.invokeAndWait(application); + } catch (InvocationTargetException ex) { + throw new RuntimeException(ex); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Returns the reference to the unique {@code EditorFrame} instance of the program. + * The main GUI class. + * + * @return an reference to {@code EditorFrame} + */ + public static EditorFrame getFrame(){ + return editorFrame; + } + + private static EditorFrame editorFrame; + Haptics haptics; + File[] templateFiles; + TemplateEditor[] templateCreators; + String backupDirPath; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/DiagramEditorApp.properties Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,34 @@ + +template.extension=.xml +dir.home=ccmi_editor_data +dir.templates=templates +dir.diagrams=diagrams +dir.backups=backups +dir.images=images +dir.libs=libs +dir.log=log + +dir.error_msg=Could not create the following directory: {0}\u000A\ +Please check directory permissions and try again. + +haptics_init.welcome=Select the haptic device you want to use + +#### APPLICATION PREFERENCES #### +# server.local_port +# server.remote_port +# home +# dir.diagrams +# dir.images +# dir.libs +# dir.log +# laf +# recent +# use_accessible_filechooser +# user.id +# server.address +# server.local_port +# server.remote_port +# second_voice_enabled +# haptic_device +# haptic_device.initialized +# enable_log
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/Organization Chart.xml Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<Diagram Name="Organization Chart" PrototypeDelegate="uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapePrototypePersistenceDelegate"> + <Prototypes> + <Node> + <Type>Entity</Type> + <ShapeType>Rectangle</ShapeType> + <Properties> + <Property> + <Type>Responsibility</Type> + <View Position="Inside" ShapeType="Rectangle"/> + </Property> + </Properties> + </Node> + <Edge> + <Type>Connection</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + </Prototypes> +</Diagram>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/Tube.xml Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<Diagram Name="Tube" PrototypeDelegate="uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapePrototypePersistenceDelegate"> + <Prototypes> + <Node> + <Type>Stations</Type> + <ShapeType>Circle</ShapeType> + <Properties/> + </Node> + <Edge> + <Type>Central Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + <Edge> + <Type>Victoria Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + <Edge> + <Type>Jubilee Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + <Edge> + <Type>Piccadilly Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + <Edge> + <Type>Circle Line</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + </Edge> + </Prototypes> +</Diagram>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/main/UML Diagram.xml Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<Diagram Name="UML Diagram" PrototypeDelegate="uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapePrototypePersistenceDelegate"> + <Prototypes> + <Node> + <Type>Class</Type> + <ShapeType>Rectangle</ShapeType> + <Properties> + <Property> + <Type>attributes</Type> + <View Position="Inside" ShapeType="Transparent"/> + <Modifiers> + <Modifier id="0"> + <Type>static</Type> + <View Bold="false" Italic="true" Prefix="" Suffix="" Underline="false"/> + </Modifier> + <Modifier id="1"> + <Type>protected</Type> + <View Bold="false" Italic="false" Prefix="#" Suffix="" Underline="false"/> + </Modifier> + <Modifier id="2"> + <Type>private</Type> + <View Bold="false" Italic="false" Prefix="-" Suffix="" Underline="false"/> + </Modifier> + </Modifiers> + </Property> + <Property> + <Type>operations</Type> + <View Position="Inside" ShapeType="Transparent"/> + <Modifiers> + <Modifier id="0"> + <Type>static</Type> + <View Bold="false" Italic="true" Prefix="" Suffix="" Underline="false"/> + </Modifier> + <Modifier id="1"> + <Type>protected</Type> + <View Bold="false" Italic="false" Prefix="#" Suffix="" Underline="false"/> + </Modifier> + <Modifier id="2"> + <Type>private</Type> + <View Bold="false" Italic="false" Prefix="-" Suffix="" Underline="false"/> + </Modifier> + </Modifiers> + </Property> + </Properties> + </Node> + <Edge> + <Type>Association</Type> + <LineStyle>Solid</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + <Heads> + <Head Head="Tail" headLabel="from"/> + <Head Head="V" headLabel="to"/> + </Heads> + </Edge> + <Edge> + <Type>Generalization</Type> + <LineStyle>Dashed</LineStyle> + <MinAttachedNodes>2</MinAttachedNodes> + <MaxAttachedNodes>2</MaxAttachedNodes> + <Heads> + <Head Head="Tail" headLabel="sub class"/> + <Head Head="Triangle" headLabel="super class"/> + </Heads> + </Edge> + </Prototypes> +</Diagram>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/AwarenessMessage.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,80 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +public class AwarenessMessage extends Message { + + public AwarenessMessage(long timestamp, Name name, String diagram, DiagramEventActionSource source) { + super(timestamp, diagram, source); + this.name = name; + } + + public AwarenessMessage(Name name, String diagram, DiagramEventActionSource source) { + super(diagram, source); + this.name = name; + } + + public AwarenessMessage(Name name, String diagram, String source) { + super(diagram, source); + this.name = name; + } + + @Override + public Name getName() { + return name; + } + + public static Name valueOf(String n){ + Name name = Name.NONE_A; + try { + name = Name.valueOf(n); + }catch(IllegalArgumentException iae){ + iae.printStackTrace(); + } + return name; + } + + public final static String NAME_POSTFIX = "_A"; + public static final String USERNAMES_SEPARATOR = "\n"; + private Name name; + private static String defaultUserName = PreferencesService.getInstance().get("user.name", System.getProperty("user.name")); + + public enum Name implements Message.MessageName { + START_A, + STOP_A, + GOON_A, + USERNAME_A, + ERROR_A, + NONE_A; + }; + + public static String getDefaultUserName(){ + synchronized(AwarenessMessage.class){ + return defaultUserName; + } + } + + public static void setDefaultUserName(String userName){ + synchronized(AwarenessMessage.class){ + defaultUserName = userName ; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/CCmIOSCBundle.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,53 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + + +import de.sciss.net.OSCBundle; + +/** + * an OSCBundle implementation for the CCmI Editor. It overrides the behaviour of + * getTimeTag() and setTimeTagAbsMillis(long when) in order to provide a + * time tag representation more suitable for the interaction logging. + * + */ +class CCmIOSCBundle extends OSCBundle { + public CCmIOSCBundle(long timestamp){ + this.timestamp = timestamp; + } + + public CCmIOSCBundle(){ + this(System.currentTimeMillis()); + } + + @Override + public long getTimeTag(){ + return timestamp; + } + + @Override + public void setTimeTagAbsMillis(long when) { + super.setTimeTagAbsMillis(when); + this.timestamp = when; + } + + private long timestamp; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/CCmIOSCPacketCodec.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,55 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import de.sciss.net.OSCPacketCodec; + +/* + * an OSC OSCPacketCodec with MODE_FAT_V1 support mode. + * + * @see OSCPacketCodec#MODE_FAT_V1 + */ +class CCmIOSCPacketCodec extends OSCPacketCodec { + + public CCmIOSCPacketCodec(){ + super(OSCPacketCodec.MODE_FAT_V1); + } + + /*@Override + protected CCmIOSCBundle decodeBundle(ByteBuffer b) throws IOException{ + OSCBundle oldFashion = super.decodeBundle(b); + CCmIOSCBundle newFashion = new CCmIOSCBundle(); + b.position("#bundle\0".length()); + long timestamp = b.getLong(); + newFashion.setTimeTagAbsMillis(timestamp); + for(int i = 0; i<oldFashion.getPacketCount();i++){ + newFashion.addPacket(oldFashion.getPacket(i)); + } + return newFashion; + } + + @Override + protected void encodeBundle(OSCBundle bndl, ByteBuffer b) throws IOException{ + super.encodeBundle(bndl, b); + super.encodeBundle(bndl, b); + b.position("#bundle\0".length()); + long timestamp = b.getLong(); + }*/ +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ClientConnectionManager.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,761 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import javax.swing.SwingUtilities; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +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; + +/** + * This is the class that manages the connection with the server. When a diagram is shared + * this class becomes responsible for actually operating the model (trough the EVT tough, by calling SwingUtilities.invokeLater ). + * If the operation is issued by the local user, than it performs the local action with local data only after + * being acknowledged by the server, else it creates the data on demand. For example upon an insert + * issued by the server, the element is created from scratch, according to the message of the server. + * + */ +public class ClientConnectionManager extends NetworkThread { + + public ClientConnectionManager(EditorTabbedPane tabbedPane) throws IOException{ + super("Network Client Thread"); + channels = new HashMap<SocketChannel, Diagram>(); + requests = new ConcurrentLinkedQueue<Request>(); + answers = new LinkedBlockingQueue<Answer>(); + pendingCommands = new LinkedList<SendCmdRequest>(); + selector = Selector.open(); + this.tabbedPane = tabbedPane; + protocol = ProtocolFactory.newInstance(); + mustSayGoodbye = false; + waitingAnswer = false; + } + + /** + * The Event Dispatching Thread communicates with this thread through a concurrent queue. + * This is the method to add requests to the queue. + * @param r the request for this thread + */ + public void addRequest(Request r){ + requests.add(r); + 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 { + Answer answer = answers.take(); + waitingAnswer = false; + return answer; + } catch (InterruptedException e) { + throw new RuntimeException(e);// must never happen + } + } + + @Override + public void run(){ + while(!mustSayGoodbye){ + try { + selector.select(); + } catch (IOException e) { + revertAllDiagrams(); + } + + if(mustSayGoodbye) + break; + + /* handle the requests for the remote server from the local users */ + handleRequests(); + + for (Iterator<SelectionKey> itr = selector.selectedKeys().iterator(); itr.hasNext();){ + SelectionKey key = itr.next(); + itr.remove(); + + if(!key.isValid()) + continue; + + if(key.isReadable()){ + SocketChannel channel = (SocketChannel)key.channel(); + Message msg = null; + try { + msg = protocol.receiveMessage(channel); + } catch (IOException e) { + revertDiagram(channel,false); + /* signal the event dispatching thread, otherwise blocked */ + 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 */ + 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); + 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 : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((String)cmd.getArgAt(0), diagram.getEdgePrototypes()); + edge = (Edge)edge.clone(); + long[] nodesToConnect = new long[cmd.getArgNum()-1]; + for(int i=0;i<nodesToConnect.length;i++) + nodesToConnect[i] = (Long)cmd.getArgAt(i+1); + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), edge, nodesToConnect, cmd.getSource())); + diagram.getCollectionModel().getMonitor().unlock(); + break; + case REMOVE_NODE : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(),node,cmd.getSource())); + break; + case REMOVE_EDGE : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0), diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(),edge,cmd.getSource())); + break; + case SET_EDGE_NAME : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0), diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetName(edge, (String)cmd.getArgAt(1),cmd.getSource())); + break; + case SET_NODE_NAME : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetName(node, (String)cmd.getArgAt(1),cmd.getSource())); + break; + case SET_PROPERTY : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetProperty( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + (String)cmd.getArgAt(3), + cmd.getSource() + )); + break; + case SET_PROPERTIES : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetProperties( + node, + (String)cmd.getArgAt(1), + cmd.getSource() + )); + break; + case CLEAR_PROPERTIES : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node,cmd.getSource())); + break; + case SET_NOTES : + int[] path = new int[cmd.getArgNum()-1]; + for(int i = 0; i< cmd.getArgNum()-1;i++){ + path[i] = (Integer)cmd.getArgAt(i); + } + final String notes = (String)cmd.getArgAt(cmd.getArgNum()-1); + diagram.getCollectionModel().getMonitor().lock(); + treeNode = Finder.findTreeNode(path, (DiagramTreeNode)diagram.getTreeModel().getRoot()); + diagram.getCollectionModel().getMonitor().unlock();; + SwingUtilities.invokeLater(new CommandExecutor.SetNotes(diagram.getTreeModel(),treeNode, notes,cmd.getSource())); + break; + case ADD_PROPERTY : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.AddProperty( + node, + (String)cmd.getArgAt(1), + (String)cmd.getArgAt(2), + cmd.getSource() + )); + break; + case REMOVE_PROPERTY : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + cmd.getSource())); + break; + case SET_MODIFIERS : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + indexes = new HashSet<Integer>(cmd.getArgNum()-3); + for(int i=3;i<cmd.getArgNum();i++){ + indexes.add((Integer)cmd.getArgAt(i)); + } + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetModifiers( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + indexes, + cmd.getSource())); + break; + case SET_ENDLABEL : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel(edge, node,(String)cmd.getArgAt(2),cmd.getSource())); + break; + case SET_ENDDESCRIPTION : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription( + edge, + node, + (Integer)cmd.getArgAt(2), + cmd.getSource() + )); + break; + case TRANSLATE_NODE : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.Translate( + node, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + (Double)cmd.getArgAt(3), + (Double)cmd.getArgAt(4), + cmd.getSource())); + break; + case TRANSLATE_EDGE : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.Translate( + edge, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + (Double)cmd.getArgAt(3), + (Double)cmd.getArgAt(4), + cmd.getSource())); + break; + case BEND : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + Point2D bendStart = null; + if(cmd.getArgNum() == 5){ + bendStart = new Point2D.Double((Double)cmd.getArgAt(3),(Double)cmd.getArgAt(4)); + } + SwingUtilities.invokeLater(new CommandExecutor.Bend( + edge, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + bendStart, + cmd.getSource())); + break; + case STOP_EDGE_MOVE : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge,cmd.getSource())); + break; + case STOP_NODE_MOVE : + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(node,cmd.getSource())); + break; + } + }else if(msg instanceof Reply){ + Reply reply = (Reply)msg; + /* log the reply through the interaction logger, if any */ + Reply.log(reply); + sendCmdRequest = null; + for(SendCmdRequest scr : pendingCommands){ + if(scr.matches(channel, reply.getDiagram())){ + sendCmdRequest = scr; + break; + } + } + assert(sendCmdRequest != null); + pendingCommands.remove(sendCmdRequest); + switch(reply.getName()){ + case INSERT_NODE_R : + case INSERT_EDGE_R : + SwingUtilities.invokeLater(new CommandExecutor.Insert(diagram.getCollectionModel(), sendCmdRequest.element, null, reply.getSource())); + break; + case REMOVE_EDGE_R : + case REMOVE_NODE_R : + SwingUtilities.invokeLater(new CommandExecutor.Remove(diagram.getCollectionModel(), sendCmdRequest.element,reply.getSource())); + break; + case SET_EDGE_NAME_R : + case SET_NODE_NAME_R : + SwingUtilities.invokeLater(new CommandExecutor.SetName(sendCmdRequest.element, (String)sendCmdRequest.cmd.getArgAt(1),reply.getSource())); + break; + case SET_PROPERTY_R : + SwingUtilities.invokeLater(new CommandExecutor.SetProperty( + (Node)sendCmdRequest.element, + (String)sendCmdRequest.cmd.getArgAt(1), + (Integer)sendCmdRequest.cmd.getArgAt(2), + (String)sendCmdRequest.cmd.getArgAt(3), + reply.getSource() + )); + break; + case SET_PROPERTIES_R : + Node n = (Node)sendCmdRequest.element; + SwingUtilities.invokeLater(new CommandExecutor.SetProperties( + n, + (String)sendCmdRequest.cmd.getArgAt(1), + reply.getSource())); + break; + case CLEAR_PROPERTIES_R : + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties((Node)sendCmdRequest.element,reply.getSource())); + break; + case SET_NOTES_R : + SwingUtilities.invokeLater(new CommandExecutor.SetNotes( + diagram.getTreeModel(), + ((SendTreeCmdRequest)sendCmdRequest).treeNode, + (String)sendCmdRequest.cmd.getArgAt(sendCmdRequest.cmd.getArgNum()-1), + reply.getSource())); + break; + case ADD_PROPERTY_R : + SwingUtilities.invokeLater(new CommandExecutor.AddProperty( + (Node)sendCmdRequest.element, + (String)sendCmdRequest.cmd.getArgAt(1), + (String)sendCmdRequest.cmd.getArgAt(2), + reply.getSource())); + break; + case REMOVE_PROPERTY_R : + SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( + (Node)sendCmdRequest.element, + (String)sendCmdRequest.cmd.getArgAt(1), + (Integer)sendCmdRequest.cmd.getArgAt(2), + reply.getSource())); + break; + case SET_MODIFIERS_R : + indexes = new HashSet<Integer>(sendCmdRequest.cmd.getArgNum()-3); + for(int i=3;i<sendCmdRequest.cmd.getArgNum();i++){ + indexes.add((Integer)sendCmdRequest.cmd.getArgAt(i)); + } + SwingUtilities.invokeLater(new CommandExecutor.SetModifiers( + (Node)sendCmdRequest.element, + (String)sendCmdRequest.cmd.getArgAt(1), + (Integer)sendCmdRequest.cmd.getArgAt(2), + indexes, + reply.getSource())); + break; + case SET_ENDLABEL_R : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)sendCmdRequest.cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel( + (Edge)sendCmdRequest.element, + node, + (String)sendCmdRequest.cmd.getArgAt(2), + reply.getSource())); + break; + case SET_ENDDESCRIPTION_R : + synchronized(diagram.getCollectionModel().getMonitor()){ + node = Finder.findNode((Long)sendCmdRequest.cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + } + SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription( + (Edge)sendCmdRequest.element, + node, + /* if the endDescription ain't included then we have to set it to null ( = NONE )*/ + sendCmdRequest.cmd.getArgNum() == 3 ? (Integer)sendCmdRequest.cmd.getArgAt(2): -1, + reply.getSource())); + break; + case TRANSLATE_NODE_R : + node = (Node)sendCmdRequest.element; + SwingUtilities.invokeLater(new CommandExecutor.Translate( + node, + new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), + (Double)sendCmdRequest.cmd.getArgAt(3), + (Double)sendCmdRequest.cmd.getArgAt(4), + reply.getSource())); + break; + case TRANSLATE_EDGE_R : + edge = (Edge)sendCmdRequest.element; + SwingUtilities.invokeLater(new CommandExecutor.Translate( + edge, + new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), + (Double)sendCmdRequest.cmd.getArgAt(3), + (Double)sendCmdRequest.cmd.getArgAt(4), + reply.getSource() + )); + break; + case BEND_R : + edge = (Edge)sendCmdRequest.element; + Point2D bendStart = null; + if(sendCmdRequest.cmd.getArgNum() == 5){ + bendStart = new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(3),(Double)sendCmdRequest.cmd.getArgAt(4)); + } + SwingUtilities.invokeLater(new CommandExecutor.Bend( + edge, + new Point2D.Double((Double)sendCmdRequest.cmd.getArgAt(1),(Double)sendCmdRequest.cmd.getArgAt(2)), + bendStart, + reply.getSource() + )); + break; + case STOP_EDGE_MOVE_R : + edge = (Edge)sendCmdRequest.element; + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge,reply.getSource())); + break; + case STOP_NODE_MOVE_R : + node = (Node)sendCmdRequest.element; + 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(),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 if(msg instanceof LockMessage){ // lock message from the server + try { + answers.put(new LockAnswer((LockMessage)msg)); + } 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; + } + } + } + } + } + } + /* this part is never reached out unless the thread is shut down */ + for(SocketChannel channel : channels.keySet()){ + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();} + } + } + + public void shutdown(){ + mustSayGoodbye = true; + selector.wakeup(); + } + + private void handleRequests() { + while(!requests.isEmpty()){ + Request request = requests.poll(); + if(request instanceof AddDiagramRequest){ + AddDiagramRequest adr = (AddDiagramRequest)request; + try { + adr.channel.configureBlocking(false); + adr.channel.register(selector, SelectionKey.OP_READ); + channels.put(adr.channel, adr.diagram); + } catch (IOException ioe) { + /* something went wrong, turn the diagram back into a local one */ + /* put the entry in channels just for a moment as it will be used in revertDiagram */ + channels.put(adr.channel, adr.diagram); + revertDiagram(adr.channel,false); + } + }else if(request instanceof RmDiagramRequest){ // user un-shared a diagram + RmDiagramRequest rdr = (RmDiagramRequest)request; + Set<Map.Entry<SocketChannel, Diagram>> entryset = channels.entrySet(); + SocketChannel channel = null; + for(Map.Entry<SocketChannel, Diagram> entry : entryset){ + if(entry.getValue().getName().equals(rdr.diagramName)){ + 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()); + if(!channels.containsKey(scr.channel)) + continue; // commands issued after reverting a diagram are dropped + pendingCommands.add(scr); + try{ + protocol.send(scr.channel, scr.cmd); + }catch(IOException e){ + /* the pending commands is normally removed upon reply receive */ + pendingCommands.remove(scr); + revertDiagram(scr.channel,false); + } + }else if(request instanceof SendLockRequest){ + SendLockRequest slr = (SendLockRequest)request; + try { + protocol.send(slr.channel,slr.lock); + } catch (IOException e) { + revertDiagram(slr.channel,false); + 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); //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,final boolean userRequest){ + /* from now on all the commands using this channel will be dropped */ + final Diagram diagram = channels.remove(c); + if(diagram == null) + return; + try{c.close();}catch(IOException ioe){ioe.printStackTrace();} + SwingUtilities.invokeLater(new Runnable(){ + @Override + public void run() { + for(int i=0; i< tabbedPane.getTabCount();i++){ + DiagramPanel dPanel = (DiagramPanel)tabbedPane.getComponentAt(i); + if(dPanel.getDiagram() instanceof NetDiagram){ + NetDiagram netDiagram = (NetDiagram)dPanel.getDiagram(); + if( netDiagram.getDelegate().equals(diagram)){ + /* set the old (unwrapped) diagram as the current one */ + dPanel.setAwarenessPanelEnabled(false); + dPanel.setDiagram(diagram); + break; + } + } + } + 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()) + ); + } + }); + + } + + private void revertAllDiagrams(){ + for(SocketChannel c : channels.keySet()) + try{c.close();}catch(IOException ioe){ioe.printStackTrace();} + channels.clear(); + + SwingUtilities.invokeLater(new Runnable(){ + @Override + public void run() { + for(int i=0; i< tabbedPane.getTabCount();i++){ + DiagramPanel dPanel = (DiagramPanel)tabbedPane.getComponentAt(i); + if(dPanel.getDiagram() instanceof NetDiagram){ + NetDiagram netDiagram = (NetDiagram)dPanel.getDiagram(); + /* set the old (unwrapped) diagram as the current one */ + dPanel.setAwarenessPanelEnabled(false); + dPanel.setDiagram(netDiagram.getDelegate()); + } + } + SpeechOptionPane.showMessageDialog( + tabbedPane, + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.connections")); + } + }); + } + + public interface Request {}; + + public interface DiagramRequest extends Request {}; + + public static class AddDiagramRequest implements DiagramRequest { + public AddDiagramRequest(SocketChannel channel, Diagram diagram){ + this.channel = channel; this.diagram = diagram; + } + public SocketChannel channel; + public Diagram diagram; + } + + public static class RmDiagramRequest implements DiagramRequest { + public RmDiagramRequest(String diagramName){ + this.diagramName = diagramName; + } + public String diagramName; + } + + public static class SendCmdRequest implements Request { + public SendCmdRequest(Command cmd, SocketChannel channel, DiagramElement element ){ + this.cmd = cmd; this.element = element;this.channel = channel; + } + + public boolean matches(SocketChannel c,String diagramName){ + return(diagramName.equals(cmd.getDiagram())&&c.socket().getInetAddress().equals(channel.socket().getInetAddress())); + } + public DiagramElement element; + public SocketChannel channel; + public Command cmd; + } + + public static class SendTreeCmdRequest extends SendCmdRequest{ + public SendTreeCmdRequest( Command cmd,SocketChannel channel,DiagramTreeNode treeNode) { + super(cmd,channel,null); + this.treeNode = treeNode; + } + public DiagramTreeNode treeNode; + public SocketChannel channel; + public Command cmd; + } + + public static class SendLockRequest implements Request { + public SendLockRequest (SocketChannel channel, LockMessage lock){ + this.channel = channel; + this.lock = lock; + } + public SocketChannel channel; + public LockMessage lock; + } + + public 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){ + this.message = answer; + } + public LockMessage message; + } + + public static class RevertedDiagramAnswer implements Answer{} + + private Node node; + private Edge edge; + private DiagramTreeNode treeNode; + private Set<Integer> indexes; + private SendCmdRequest sendCmdRequest; + /* for each server hold the diagram it shares with it */ + private Map<SocketChannel, Diagram> channels; + private ConcurrentLinkedQueue<Request> requests; + private BlockingQueue<Answer> answers; + private LinkedList<SendCmdRequest> pendingCommands; + private Selector selector; + private EditorTabbedPane tabbedPane; + private Protocol protocol; + private volatile boolean waitingAnswer; + private volatile boolean mustSayGoodbye; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Command.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,147 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; +import uk.ac.qmul.eecs.ccmi.utils.CharEscaper; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * A command message that triggers an update in the model of the target diagram. Possible updates + * are those listed in the {@code MessageName} enum. + * Command messages are issued by both clients and server. Clients commands are sent after a + * user action. When the server receives a command it broadcasts it to all the clients but the one + * from which the command was from. + * + */ +public class Command extends Message { + public Command(Name name, String diagram, Object[] args, long timestamp, DiagramEventSource source){ + super(timestamp,diagram,source); + this.name = name; + this.args = args; + } + + 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, DiagramEventSource source){ + this(name, diagram, source); + } + + 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; + } + + public Object getArgAt(int index) { + return args[index]; + } + + public Object[] getArgs(){ + return args; + } + + public int getArgNum(){ + return args.length; + } + + + /** + * Utility method to log, through the interaction log, the receipt of a command + * @param cmd the received command + * @param action a further description of the action that triggered this command + */ + public static void log(Command cmd, String action){ + if(cmd.getName() != Command.Name.LOCAL && cmd.getName() != Command.Name.BEND + && cmd.getName() != Command.Name.TRANSLATE_EDGE && cmd.getName() != Command.Name.TRANSLATE_NODE){ + StringBuilder builder = new StringBuilder(cmd.getName().toString()); + builder.append(' ').append(cmd.getDiagram()); + for(int i=0; i<cmd.getArgNum();i++){ + builder.append(' ').append(cmd.getArgAt(i)); + } + /* replace newlines for notes so that the log has them in one line only */ + if(cmd.getName() == Command.Name.SET_NOTES){ + InteractionLog.log("SERVER", action, CharEscaper.replaceNewline(builder.toString())); + return; + } + InteractionLog.log("SERVER", action, builder.toString()); + } + } + + public static Name valueOf(String n){ + Name name = Name.NONE; + try { + name = Name.valueOf(n); + }catch(IllegalArgumentException iae){ + iae.printStackTrace(); + } + return name; + } + + private Name name; + private Object[] args; + + public static enum Name implements Message.MessageName { + NONE, + LIST, + GET, + LOCAL, + INSERT_EDGE, + INSERT_NODE, + REMOVE_NODE, + REMOVE_EDGE, + SET_NODE_NAME, + SET_EDGE_NAME, + SET_PROPERTY, + SET_PROPERTIES, + CLEAR_PROPERTIES, + SET_NOTES, + ADD_PROPERTY, + REMOVE_PROPERTY, + SET_MODIFIERS, + SET_ENDDESCRIPTION, + SET_ENDLABEL, + TRANSLATE_NODE, + TRANSLATE_EDGE, + BEND, + STOP_EDGE_MOVE, + STOP_NODE_MOVE, + /** + * 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; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/CommandExecutor.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,397 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.awt.Component; +import java.awt.geom.Point2D; +import java.util.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.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; +/** + * This class inner classes are used to invoke commands on the Event Dispatching thread by + * the networking threads receiving replies or commands by the server. Its aim + * is to preserve reference integrity in the changeover between the two threads + * + * + */ +public abstract class CommandExecutor implements Runnable { + protected CommandExecutor(DiagramEventSource source){ + this.source = source; + } + + protected DiagramEventSource source; + public static class Insert extends CommandExecutor{ + public Insert(CollectionModel<Node,Edge> m, DiagramElement de, long[] nds, DiagramEventSource source){ + super(source); + element = de; + model = m; + nodes = nds; + } + @Override + public void run() { + model.getMonitor().lock(); + if(element instanceof Node){ + model.insert((Node)element,source); + }else{ + Edge edge = (Edge)element; + if(nodes != null){ + List<DiagramNode> nodesToConnect = new ArrayList<DiagramNode>(nodes.length); + /* retrieve the nodes to connect by the id, conveyed in the message */ + for(int i = 0; i<nodes.length; i++){ + Node attachedNode = Finder.findNode(nodes[i],model.getNodes()); + nodesToConnect.add(attachedNode); + } + try { + edge.connect(nodesToConnect); + } catch (ConnectNodesException e) { + throw new RuntimeException();//this must never happen as the check is done by the local client before issuing the command + } + } + model.insert(edge,source); + } + model.getMonitor().unlock(); + } + private CollectionModel<Node,Edge> model; + private DiagramElement element; + private long[] nodes; + } + + public static class Remove extends CommandExecutor{ + public Remove(CollectionModel<Node,Edge> m, DiagramElement de, DiagramEventSource source) { + super(source); + model = m; + element = de; + } + @Override + public void run() { + model.getMonitor().lock(); + model.takeOut(element,source); + model.getMonitor().unlock(); + } + private CollectionModel<Node,Edge> model; + private DiagramElement element; + } + + public static class SetName extends CommandExecutor { + public SetName(DiagramElement de, String n, DiagramEventSource source){ + super(source); + element = de; + name = n; + } + @Override + public void run(){ + element.getMonitor().lock(); + element.setName(name,source); + element.getMonitor().unlock(); + } + private String name; + private DiagramElement element; + } + + + public static class SetProperty extends CommandExecutor{ + public SetProperty(Node n, String t, Integer i, String v, DiagramEventSource source){ + super(source); + node = n; + type = t; + index = i; + value = v; + } + + @Override + public void run(){ + node.getMonitor().lock(); + node.setProperty(type, index, value,source); + node.getMonitor().unlock(); + } + + private Node node; + private String type; + private Integer index; + private String value; + } + + public static class SetProperties extends CommandExecutor { + public SetProperties(Node n, String p, DiagramEventSource source){ + super(source); + node = n; + propertiesAsString = p; + } + @Override + public void run(){ + node.getMonitor().lock(); + NodeProperties properties = node.getProperties(); + properties.fill(propertiesAsString); + node.setProperties(properties,source); + node.getMonitor().unlock(); + } + private Node node; + private String propertiesAsString; + } + + public static class ClearProperties extends CommandExecutor { + public ClearProperties(Node n, DiagramEventSource source){ + super(source); + node = n; + } + @Override + public void run(){ + node.getMonitor().lock(); + node.clearProperties(source); + node.getMonitor().unlock(); + } + private Node node; + } + + public static class SetNotes extends CommandExecutor{ + public SetNotes(TreeModel<Node,Edge> m, DiagramTreeNode tn, String n, DiagramEventSource source){ + super(source); + model = m; + treeNode = tn; + notes = n; + } + + @Override + public void run(){ + model.getMonitor().lock(); + model.setNotes(treeNode, notes,source); + model.getMonitor().unlock(); + } + private DiagramTreeNode treeNode; + private String notes; + private TreeModel<Node,Edge> model; + } + + public static class AddProperty extends CommandExecutor { + public AddProperty(Node n, String t, String v, DiagramEventSource source){ + super(source); + node = n; + type = t; + value = v; + } + @Override + public void run(){ + node.getMonitor().lock(); + node.addProperty(type, value,source); + node.getMonitor().unlock(); + } + private Node node; + private String type; + private String value; + } + + public static class RemoveProperty extends CommandExecutor { + public RemoveProperty(Node n, String t, int i, DiagramEventSource source){ + super(source); + node = n; + type = t; + index = i; + } + + @Override + public void run(){ + node.getMonitor().lock(); + node.removeProperty(type, index,source); + node.getMonitor().unlock(); + } + + private Node node; + private String type; + private int index; + } + + public static class SetModifiers extends CommandExecutor { + public SetModifiers(Node n,String t,Integer v, Set<Integer> i, DiagramEventSource source){ + super(source); + node = n; + type = t; + value = v; + indexes = i; + } + @Override + public void run(){ + node.getMonitor().lock(); + node.setModifierIndexes(type, value, indexes,source); + node.getMonitor().unlock(); + } + private Node node; + private String type; + private Integer value; + private Set<Integer> indexes; + } + + public static class SetEndLabel extends CommandExecutor { + public SetEndLabel(Edge e, Node n, String l, DiagramEventSource source){ + super(source); + edge = e; + node = n; + label = l; + } + @Override + public void run(){ + edge.getMonitor().lock(); + edge.setEndLabel(node, label,source); + edge.getMonitor().unlock(); + } + private Node node; + private Edge edge; + private String label; + } + + public static class SetEndDescription extends CommandExecutor { + public SetEndDescription(Edge e, Node n, int i, DiagramEventSource source){ + super(source); + edge = e; + node = n; + index = i; + } + @Override + public void run(){ + edge.getMonitor().lock(); + edge.setEndDescription(node, index,source); + edge.getMonitor().unlock(); + } + private Node node; + private Edge edge; + private int index; + } + + public static class Translate extends CommandExecutor { + public Translate(GraphElement ge, Point2D p, Double x, Double y, DiagramEventSource source){ + super(source); + element = ge; + point = p; + dx = x; + dy = y; + } + + @Override + public void run(){ + 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; + private Point2D point; + private Double dx; + private Double dy; + } + + public static class StartMove extends CommandExecutor { + public StartMove(GraphElement ge, Point2D p, DiagramEventSource source){ + super(source); + element = ge; + point = p; + } + @Override + public void run(){ + 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, DiagramEventSource source){ + super(source); + edge = e; + point = p; + bendStart = bs; + } + @Override + public void run(){ + edge.getMonitor().lock(); + if(bendStart != null) + edge.startMove(bendStart,source); + edge.bend(point,source); + edge.getMonitor().unlock(); + } + private Edge edge; + private Point2D point; + private Point2D bendStart; + } + + public static class StopMove extends CommandExecutor { + public StopMove(GraphElement ge, DiagramEventSource source){ + super(source); + element = ge; + } + @Override + public void run(){ + 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, DiagramEventSource source){ + super(source); + message = msg; + parentComponent = c; + } + @Override + public void run(){ + SpeechOptionPane.showMessageDialog(parentComponent, message); + } + Component parentComponent; + String message; + } + + @Override + public abstract void run(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramAlreadySharedException.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,32 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * If the user tries to share the same diagram twice, they will get this exception thrown. + * + */ +@SuppressWarnings("serial") +public class DiagramAlreadySharedException extends DiagramShareException { + DiagramAlreadySharedException(String msg){ + super(msg); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramDownloader.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,87 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + + +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; + +/** + * A {@code SwingWorker} that takes on the communication with the server in the very first phase of the + * connection for a new diagram. It handles the download of the list of diagrams available for sharing on the server + * and, once the user has chosen one, it downloads the diagram into the local editor. + * Since this tasks can take a long time due to network delay and the interaction has not yet started + * a {@code SwingWorker} is used so that the user interface won't get stuck in the process, the user + * being able to cancel the job and to go back to the diagram editor. + * + */ +public class DiagramDownloader extends SpeechOptionPane.ProgressDialogWorker<String,Void> { + + public DiagramDownloader(SocketChannel channel, String target, int task){ + this.channel = channel; + this.task = task; + if(task == CONNECT_AND_DOWNLOAD_LIST_TASK) + this.address = target; + else + this.diagramName = target; + } + + @Override + protected String doInBackground() throws Exception { + 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_TASK : + protocol.send(channel, new Command(Command.Name.LIST,"",DiagramEventSource.NONE)); + break; + case DOWNLOAD_DIAGRAM_TASK : + protocol.send(channel, new Command(Command.Name.GET ,diagramName,DiagramEventSource.NONE)); + } + Reply reply = protocol.receiveReply(channel); + switch(reply.getName()){ + case ERROR_R : + throw new DiagramShareException(reply.getMessage()); + case LIST_R : + String result = new String(reply.getMessage()); + if("".equals(result)) + return null; + return result; + case GET_R : + return reply.getMessage(); + default : throw new RuntimeException(); + } + } + + public static 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 int task; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramEventActionSource.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,151 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; + +/** + * This class represent a source of an editing action. An editing action is initiated when + * the user gets the lock on a certain element and terminates when the user after changing + * the diagram model somehow yields the lock back to the server. + * + */ +public class DiagramEventActionSource extends DiagramEventSource{ + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public DiagramEventActionSource (DiagramEventSource eventSource, Command.Name cmd, long elementID, String elementName) { + super(eventSource); + this.cmd = cmd; + this.elementID = elementID; + this.saveID = elementID; + this.elementName = elementName; + userName = AwarenessMessage.getDefaultUserName(); + timestamp = System.currentTimeMillis(); + } + + public Command.Name getCmd() { + return cmd; + } + + public long getElementID(){ + return elementID; + } + + public void setElementID(long ID){ + elementID = ID; + } + + long getSaveID(){ + return saveID; + } + + public String getElementName(){ + return elementName; + } + + public void setElementName(String elementName){ + this.elementName = elementName; + } + + public void setUserName(String name){ + this.userName = name; + } + + public String getUserName(){ + return userName; + } + + /** + * + * The local user never gets this informations from itself are they are attached to + * AwernessMessages they receive only from other users. Therefore instances of this class + * are never considered local. + */ + @Override + public boolean isLocal(){ + return false; + } + + /** + * Returns an instance of {@code DiagramEventActionSource} out of a + * String passed as argument + * @param s a string representation of a {@code DiagramEventActionSource} instance, as + * returned by toString. + * @return an instance of {@code DiagramEventActionSource} + */ + public static DiagramEventActionSource valueOf(String s){ + if(s.isEmpty()) + return NULL; + String[] strings = s.split(SEPARATOR); + long id = Long.parseLong(strings[1]); + String elementName = strings[2]; + long timestamp = Long.parseLong(strings[3]); + DiagramEventSource eventSource = DiagramEventSource.valueOf(strings[5]); + DiagramEventActionSource toReturn = new DiagramEventActionSource(eventSource,Command.Name.valueOf(strings[0]),id,elementName); + toReturn.setUserName(strings[4]); + toReturn.setTimestamp(timestamp); + return toReturn; + } + + /** + * Encodes this object into a String. the encoding is done by concatenating the command name + * with the string representation of the event source. the command name is encoded in a fixed length + * string and padded with white spaces if such length is greater than the command name's. + */ + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(cmd.name()); + builder.append(SEPARATOR); + builder.append(elementID); + builder.append(SEPARATOR); + builder.append(elementName); + builder.append(SEPARATOR); + builder.append(timestamp); + builder.append(SEPARATOR); + builder.append(userName); + builder.append(SEPARATOR); + builder.append(super.toString()); + return builder.toString(); + } + + + + private String userName; + private Command.Name cmd; + private long elementID; + private long saveID; + private String elementName; + private static final String SEPARATOR = "\n"; + private long timestamp; + + public static DiagramEventActionSource NULL = new DiagramEventActionSource(DiagramEventSource.NONE,Command.Name.NONE,-1,""){ + @Override + public String toString(){ + return ""; + } + }; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/DiagramShareException.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,32 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * This exception is thrown when a problem occurred in the process of sharing a diagram + * via the server with other remote users + * + */ +@SuppressWarnings("serial") +public class DiagramShareException extends Exception { + public DiagramShareException(String arg0) { + super(arg0); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/LockMessage.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,141 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * This class represents a lock message,through which the clients get exclusivity on + * editing the elements of the diagram. The class is used by both the client, + * to request a lock, and the server, to acknowledge the success/failure of the request. + * + * The argument of the message can be either the path to a three node or the id of a diagram element. + * The former is used for lock for notes editing which can concern every tree node in the tree, + * the latter for all the other types of lock as they only concern diagram elements. + * The path to a tree node is a sequence of integers representing the index of the children + * from the root to the affected node. So for instance 4,2,3 would be the third son of the + * second son of the fourth son of the root node. + */ +public class LockMessage extends Message { + + /** + * Creates a lock message for the given diagram and with the given timestamp. + * + * @param name the message name + * @param timestamp the time when the message was issued + * @param diagram the name of the diagram this message refers to + * @param args arguments of the message + * @param source the source that generated this message see {@link DiagramEventActionSource} + */ + public LockMessage(Name name, long timestamp, String diagram, Object[] args, Object source) { + super(timestamp, diagram,source); + this.args = args; + this.name = name; + } + + /** + * Creates a lock message for the given diagram and timestamp of the moment + * the message is created. + * + * @param name the message name + * @param diagram the name of the diagram this message refers to + * @param args arguments of the message + * @param source the source that generated this message see {@link DiagramEventActionSource} + */ + 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, DiagramEventActionSource source) { + this(name,timestamp,diagram,new Object[]{id},source); + } + + public LockMessage(Name name, String diagram, long id, DiagramEventActionSource source){ + this(name,diagram,new Object[]{id},source); + } + + @Override + public Name getName() { + return name; + } + + public Object getArgAt(int index) { + return args[index]; + } + + public Object[] getArgs(){ + return args; + } + + public int getArgNum(){ + return args.length; + } + + @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){ + iae.printStackTrace(); + return Name.NONE_L; + } + return name; + } + + /** used to distinguish between different kinds of messages. */ + 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; + private Object[] args; + + /** + * enum containing all the possible lock messages that can be exchanged + * between server and client in either direction. + */ + public static enum Name implements Message.MessageName { + GET_DELETE_L, + GET_NAME_L, + GET_PROPERTIES_L, + GET_EDGE_END_L, + GET_MOVE_L, + GET_NOTES_L, + GET_BOOKMARK_L, + GET_MUST_EXIST_L, + + YIELD_DELETE_L, + YIELD_NAME_L, + YIELD_PROPERTIES_L, + YIELD_EDGE_END_L, + YIELD_MOVE_L, + YIELD_NOTES_L, + YIELD_BOOKMARK_L, + YIELD_MUST_EXISTS_L, + + YES_L, + NO_L, + NONE_L; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/LockMessageConverter.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,103 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.gui.Lock; + +/** + * A utility class providing static methods to convert a Lock into the LockMessage + * conveying it. And the other way around. + * + * + */ +public class LockMessageConverter { + /** + * Creates a lock message out of a lock passed as argument + * @param lock the lock to be converted + * @param isGet whether it's a "get-lock" or "yield-lock" message + * @return the lock message + */ + public static LockMessage.Name getLockMessageNamefromLock(Lock lock, boolean isGet){ + LockMessage.Name name = LockMessage.Name.NONE_L; + switch (lock){ + case DELETE : name = ((isGet) ? LockMessage.Name.GET_DELETE_L : LockMessage.Name.YIELD_DELETE_L); + break; + case NAME : name = (isGet) ? LockMessage.Name.GET_NAME_L : LockMessage.Name.YIELD_NAME_L ; + break; + case PROPERTIES : name = (isGet) ? LockMessage.Name.GET_PROPERTIES_L : LockMessage.Name.YIELD_PROPERTIES_L ; + break; + case EDGE_END : name = (isGet) ? LockMessage.Name.GET_EDGE_END_L : LockMessage.Name.YIELD_EDGE_END_L ; + break; + case MOVE : name = (isGet) ? LockMessage.Name.GET_MOVE_L : LockMessage.Name.YIELD_MOVE_L ; + break; + case NOTES : name = (isGet) ? LockMessage.Name.GET_NOTES_L : LockMessage.Name.YIELD_NOTES_L ; + break; + case BOOKMARK : name = (isGet) ? LockMessage.Name.GET_BOOKMARK_L : LockMessage.Name.YIELD_BOOKMARK_L ; + break; + case MUST_EXIST : name = (isGet) ? LockMessage.Name.GET_MUST_EXIST_L : LockMessage.Name.YIELD_MUST_EXISTS_L; + break; + } + return name; + } + + /** + * Returns the lock conveyed by the lock message passed as argument + * @param name the lock message name @see LockMessage.Name + * @return the conveyed lock + */ + public static Lock getLockFromMessageName(LockMessage.Name name){ + Lock lock = Lock.NONE; + switch(name){ + case GET_DELETE_L : + case YIELD_DELETE_L : + lock = Lock.DELETE; + break; + case GET_NAME_L : + case YIELD_NAME_L : + lock = Lock.NAME; + break; + case GET_PROPERTIES_L : + case YIELD_PROPERTIES_L : + lock = Lock.PROPERTIES; + break; + case GET_EDGE_END_L : + case YIELD_EDGE_END_L : + lock = Lock.EDGE_END; + break; + case GET_MOVE_L : + case YIELD_MOVE_L: + lock = Lock.MOVE; + break; + case GET_NOTES_L : + case YIELD_NOTES_L : + lock = Lock.NOTES; + break; + case GET_BOOKMARK_L : + case YIELD_BOOKMARK_L : + lock = Lock.BOOKMARK; + break; + case GET_MUST_EXIST_L : + case YIELD_MUST_EXISTS_L: + lock = Lock.MUST_EXIST; + break; + } + return lock; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Message.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,64 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * A basic implementation of a message exchanged between server and client. + * + */ +public abstract class Message { + + public Message(long timestamp, String diagram, Object source){ + this.timestamp = timestamp; + this.diagram = diagram; + this.source = source; + } + + public Message(String diagram, Object source){ + this(System.currentTimeMillis(),diagram,source); + } + + public long getTimestamp() { + return timestamp; + } + + public String getDiagram(){ + 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(); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/NetDiagram.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,612 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.Queue; +import java.util.Set; + +import javax.swing.tree.TreeNode; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.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; + +/** + * + * A NetDiagram is a diagram that is shared by connecting to a server on either a remote or local host. + * That means that other users from other computers can modify the diagram model through the server. + * A NetDiagram is created by wrapping a local diagram (a diagram open in the local editor) into a NetDiagram class. + * The wrapped diagram works as a delegate. What Really changes between a local diagram and a network diagram is + * that the modelUpdater will directly affect the diagram model for the former and exchange messages with the server + * for the latter. In the case of a network diagram the changes to the model are actually made by a {@link ClientConnectionManager} + * thread upon receiving a message from the server. + * + */ +public abstract class NetDiagram extends Diagram { + + private NetDiagram(Diagram delegateDiagram){ + this.delegateDiagram = delegateDiagram; + innerModelUpdater = new InnerModelUpdater(); + } + + public static NetDiagram wrapRemoteHost(Diagram diagram, ClientConnectionManager connectionManager,SocketChannel channel){ + return new RemoteHostDiagram(diagram,connectionManager,channel); + } + + public static NetDiagram wrapLocalHost(Diagram diagram, SocketChannel channel, Queue<DiagramElement> dElements, ExceptionHandler handler){ + return new LocalHostDiagram(diagram,channel,dElements,handler); + } + + @Override + public String getName(){ + return delegateDiagram.getName(); + } + + @Override + public String toString(){ + return getName(); + } + + @Override + public PrototypePersistenceDelegate getPrototypePersistenceDelegate(){ + return delegateDiagram.getPrototypePersistenceDelegate(); + } + + @Override + public TreeModel<Node,Edge> getTreeModel(){ + return delegateDiagram.getTreeModel(); + } + + @Override + public CollectionModel<Node,Edge> getCollectionModel(){ + return delegateDiagram.getCollectionModel(); + } + + @Override + public void setName(String name) { + delegateDiagram.setName(name); + } + + @Override + public Node[] getNodePrototypes() { + return delegateDiagram.getNodePrototypes(); + } + + @Override + public Edge[] getEdgePrototypes() { + return delegateDiagram.getEdgePrototypes(); + } + + @Override + public DiagramModelUpdater getModelUpdater(){ + return innerModelUpdater; + } + + public Diagram getDelegate(){ + return delegateDiagram; + } + + public abstract 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, DiagramTreeNode treeNode); + + protected abstract void send(LockMessage lockMessage); + + protected abstract void send(AwarenessMessage awMsg); + + protected abstract boolean receiveLockAnswer(); + + private Diagram delegateDiagram; + private InnerModelUpdater innerModelUpdater; + public static String LOCALHOST_STRING = " @ localhost"; + + private class InnerModelUpdater implements DiagramModelUpdater { + @Override + public boolean getLock(DiagramTreeNode treeNode, Lock lock, DiagramEventActionSource actionSource) { + try { + sendLockMessage(treeNode,lock,true,actionSource); + }catch(IllegalArgumentException iae){ + return false; + } + return receiveLockAnswer(); + } + + @Override + public void yieldLock(DiagramTreeNode treeNode, Lock lock, DiagramEventActionSource actionSource) { + try { + sendLockMessage(treeNode,lock,false,actionSource); + }catch(IllegalArgumentException iae) {} + } + + @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()) + throw new IllegalArgumentException("it's a node no longer connected with the tree"); + for(int i=0;i<path.length-1;i++){ + args[i] = delegateDiagram.getTreeModel().getIndexOfChild(path[i], path[i+1]); + } + send(new LockMessage( + LockMessageConverter.getLockMessageNamefromLock(lock,isGettingLock), + delegateDiagram.getName(), + args, + source + )); + } + + @Override + 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()}, + makeRemote(source) + ); + }else{ + Edge edge = (Edge)element; + Object args[] = new Object[1+edge.getNodesNum()]; + args[0] = edge.getType(); + /* the args of the command will be the id's of the connected edges */ + for(int i = 1; i< args.length; i++) + args[i] = edge.getNodeAt(i-1).getId(); + cmd = new Command( + Command.Name.INSERT_EDGE, + delegateDiagram.getName(), + args, + makeRemote(source) + ); + } + send(cmd,element); + } + + @Override + public void insertInTree(DiagramElement element) { + insertInCollection(element,DiagramEventSource.TREE); + } + + @Override + 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()}, + makeRemote(source) + ); + send(cmd,element); + } + + @Override + public void takeOutFromTree(DiagramElement element) { + takeOutFromCollection(element,DiagramEventSource.TREE); + } + + @Override + 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}, + makeRemote(source)), + element); + } + + @Override + public void setNotes(DiagramTreeNode treeNode, String notes, DiagramEventSource source) { + TreeNode[] path = treeNode.getPath(); + Object[] args = new Object[path.length]; + for(int i=0;i<path.length-1;i++){ + args[i] = delegateDiagram.getTreeModel().getIndexOfChild(path[i], path[i+1]); + } + args[args.length-1] = notes; + Command cmd = new Command(Command.Name.SET_NOTES, delegateDiagram.getName(),args,makeRemote(source)); + send(cmd,treeNode); + } + + @Override + public void setProperty(Node node, String type, int index, String value, DiagramEventSource source) { + send(new Command(Command.Name.SET_PROPERTY, + delegateDiagram.getName(), + new Object[] {node.getId(),type,index,value}, + makeRemote(source)), + node + ); + } + + @Override + public void setProperties(Node node, NodeProperties properties, DiagramEventSource source) { + send(new Command(Command.Name.SET_PROPERTIES, + delegateDiagram.getName(), + new Object[] {node.getId(),properties.toString()}, + makeRemote(source)), + node + ); + + } + + @Override + public void clearProperties(Node node, DiagramEventSource source) { + send(new Command(Command.Name.CLEAR_PROPERTIES, + delegateDiagram.getName(), + new Object[] {node.getId()}, + makeRemote(source)), + node + ); + } + + @Override + public void addProperty(Node node, String type, String value, DiagramEventSource source) { + send(new Command(Command.Name.ADD_PROPERTY, + delegateDiagram.getName(), + new Object[] {node.getId(),type,value}, + makeRemote(source)), + node + ); + } + + @Override + public void removeProperty(Node node, String type, int index, DiagramEventSource source) { + send(new Command(Command.Name.REMOVE_PROPERTY, + delegateDiagram.getName(), + new Object[] {node.getId(),type,index}, + makeRemote(source)), + node + ); + } + + @Override + public void setModifiers(Node node, String type, int index, + Set<Integer> modifiers, DiagramEventSource source) { + Object args[] = new Object[modifiers.size()+3]; + args[0] = node.getId(); + args[1] = type; + args[2] = index; + int i = 0; + for(Integer I : modifiers){ + args[i+3] = I; + i++; + } + send(new Command(Command.Name.SET_MODIFIERS, delegateDiagram.getName(),args,makeRemote(source)),node); + } + + @Override + 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},makeRemote(source)), + edge + ); + } + + @Override + 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},makeRemote(source)), + edge + ); + } + + @Override + public void translate(GraphElement ge, Point2D p, double dx, double dy, DiagramEventSource source) { + double px = 0; + double py = 0; + if(p != null){ + px = p.getX(); + py = p.getY(); + } + if(ge instanceof Node){ + Node n = (Node)ge; + send(new Command(Command.Name.TRANSLATE_NODE, delegateDiagram.getName(), + new Object[] {n.getId(), px, py, dx,dy},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},makeRemote(source) + ),e); + } + } + + @Override + 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 * + * the edge is clicked down. So this variable is non null only when the first * + * bend-message is sent */ + edgeStartMovePoint = p; + } + + @Override + public void bend(Edge edge, Point2D p, 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()}, + makeRemote(source)), + edge); + else{ + send(new Command(Command.Name.BEND, delegateDiagram.getName(), + new Object[] {edge.getId(),p.getX(),p.getY(), + edgeStartMovePoint.getX(),edgeStartMovePoint.getY()}, + makeRemote(source)), + edge); + edgeStartMovePoint = null; + } + } + + @Override + 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()},makeRemote(source)), + n); + }else{ + Edge e = (Edge)ge; + send(new Command(Command.Name.STOP_EDGE_MOVE, delegateDiagram.getName(), + 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; + } + + private static class RemoteHostDiagram extends NetDiagram{ + /** + * This class wraps an existing diagram into a network diagram. + * The network diagrams returns a TreeModelNetWrap and a CollectionModelNetWrap + * when the relative getters are called + * + * @param diagram the diagram to wrap + * @param connectionManager a connected socket channel + */ + private RemoteHostDiagram(Diagram diagram, ClientConnectionManager connectionManager,SocketChannel channel){ + super(diagram); + this.channel = channel; + this.connectionManager = connectionManager; + } + + @Override + protected void send(Command cmd, DiagramElement element) { + connectionManager.addRequest(new ClientConnectionManager.SendCmdRequest(cmd, channel, element )); + } + + @Override + protected void send(Command cmd, DiagramTreeNode treeNode) { + connectionManager.addRequest(new ClientConnectionManager.SendTreeCmdRequest(cmd, channel, treeNode )); + } + + @Override + protected void send(LockMessage lockMessage){ + connectionManager.addRequest(new ClientConnectionManager.SendLockRequest(channel, lockMessage)); + } + + @Override + protected void send(AwarenessMessage awMsg){ + connectionManager.addRequest(new ClientConnectionManager.SendAwarenessRequest(channel,awMsg)); + } + + @Override + protected boolean receiveLockAnswer(){ + ClientConnectionManager.Answer answer = connectionManager.getAnswer(); + /* 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 : + return true; + case NO_L : + return false; + default : + throw new RuntimeException("message not recognized: "+name.toString()); + } + } + + @Override + public String getLabel(){ + return new StringBuilder(getName()) + .append(' ').append('@').append(' ') + .append(channel.socket().getInetAddress().getHostAddress()) + .toString(); + } + + @Override + public SocketChannel getSocketChannel(){ + return channel; + } + + @Override + public 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(); + } + + private SocketChannel channel; + private ClientConnectionManager connectionManager; + } + + private static class LocalHostDiagram extends NetDiagram { + + private LocalHostDiagram(Diagram diagram, SocketChannel channel, Queue<DiagramElement> diagramElements, ExceptionHandler handler) { + super(diagram); + this.channel = channel; + this.diagramElements = diagramElements; + this.exceptionHandler = handler; + this.protocol = ProtocolFactory.newInstance(); + } + + @Override + protected void send(Command cmd, DiagramElement element){ + switch(cmd.getName()){ + case INSERT_NODE : + case INSERT_EDGE : + case REMOVE_NODE : + case REMOVE_EDGE : + diagramElements.add(element); + break; + } + try{ + protocol.send(channel, cmd); + }catch(IOException ioe){ + switch(cmd.getName()){ + case INSERT_NODE : + case INSERT_EDGE : + case REMOVE_NODE : + case REMOVE_EDGE : + diagramElements.remove(element); + break; + } + exceptionHandler.handleException(ioe); + } + } + + @Override + protected void send(LockMessage lockMessage) { + try { + protocol.send(channel, lockMessage); + } catch (IOException ioe) { + exceptionHandler.handleException(ioe); + } + } + + @Override + protected void send(AwarenessMessage awMsg){ + try { + protocol.send(channel, awMsg); + } catch (IOException ioe) { + exceptionHandler.handleException(ioe); + } + } + + @Override + protected boolean receiveLockAnswer(){ + LockMessage answer; + try { + answer = protocol.receiveLockMessage(channel); + } catch (IOException ioe) { + exceptionHandler.handleException(ioe); + return false; + } + switch((LockMessage.Name)answer.getName()){ + case YES_L : + return true; + case NO_L : + return false; + default : + throw new RuntimeException("message not recognized: "+answer.getName().toString()); + } + } + + @Override + protected void send(Command cmd , DiagramTreeNode treeNode){ + try { + protocol.send(channel, cmd); + } catch (IOException ioe) { + exceptionHandler.handleException(ioe); + } + } + + @Override + public String getLabel(){ + return getName()+LOCALHOST_STRING; + } + + @Override + public SocketChannel getSocketChannel(){ + return channel; + } + + @Override + public void enableAwareness(AwarenessPanel panel){ + Server.getServer().getAwarenessPanelEditor().addAwarenessPanel(panel); + } + + @Override + public void disableAwareness(AwarenessPanel panel){ + Server.getServer().getAwarenessPanelEditor().removeAwarenessPanel(panel); + } + + private SocketChannel channel; + private Queue<DiagramElement> diagramElements; + private Protocol protocol; + private ExceptionHandler exceptionHandler; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/NetworkThread.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,40 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.gui.awareness.AwarenessPanelEditor; + +class NetworkThread extends Thread { + + public NetworkThread() { + super(); + panelEditor = new AwarenessPanelEditor(); + } + + public NetworkThread(String name) { + super(name); + panelEditor = new AwarenessPanelEditor(); + } + + public AwarenessPanelEditor getAwarenessPanelEditor(){ + return panelEditor; + } + + private AwarenessPanelEditor panelEditor; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/OscProtocol.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,292 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.io.IOException; +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; + +/* + * An implementation of the Protocol interface which uses OSC messages + * streamed on a TCP connection. + * + */ +class OscProtocol implements Protocol { + + OscProtocol(){ + buffer = ByteBuffer.allocate(DEFAULT_CAPACITY); + codec = new CCmIOSCPacketCodec(); + } + + @Override + public void send(SocketChannel channel, Command cmd) throws IOException { + /* OSC message args = [diagram, cmd.arg1, cmd.arg2, amd.arg2, ... , cmd.argN ] */ + bundle = new CCmIOSCBundle(cmd.getTimestamp()); + Object[] args = new Object[2+cmd.getArgNum()]; + args[0] = cmd.getDiagram(); + args[1] = cmd.getSource().toString(); + for(int i=0; i<cmd.getArgNum();i++) + args[i+2] = cmd.getArgAt(i); + + bundle.addPacket(new OSCMessage(OSC_NAME_PREFIX+cmd.getName().toString(),args)); + try{ + writeBundle(channel); + }catch(IOException u){ + throw new IOException( + /* give a more user friendly message */ + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.no_send"),u); + } + } + + @Override + public void send(SocketChannel channel, Reply reply) throws IOException { + /* OSC message args = [messageLen, diagram, message ] */ + bundle = new CCmIOSCBundle(reply.getTimestamp()); + bundle.addPacket(new OSCMessage( + OSC_NAME_PREFIX+reply.getName().toString(), + new Object[] {reply.getMessageLen(), reply.getDiagram() ,reply.getMessage(),reply.getSource().toString()} + )); + try{ + writeBundle(channel); + }catch(IOException u){ + throw new IOException( + /* give a more user friendly message */ + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.no_send"),u); + } + } + + @Override + public void send(SocketChannel channel, LockMessage lockMessage) throws IOException { + /* OSC message args = [diagram, source, lock.arg1, lock.arg2, lock.arg2, ... , lock.argN ] */ + bundle = new CCmIOSCBundle(lockMessage.getTimestamp()); + Object[] args = new Object[2+lockMessage.getArgNum()]; + args[0] = lockMessage.getDiagram(); + args[1] = lockMessage.getSource().toString(); + for(int i=0; i<lockMessage.getArgNum();i++) + args[i+2] = lockMessage.getArgAt(i); + + bundle.addPacket(new OSCMessage( + OSC_NAME_PREFIX+lockMessage.getName().toString(), + args)); + try{ + writeBundle(channel); + }catch(IOException u){ + throw new IOException( + /* give a more user friendly message */ + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.no_send"),u); + } + } + + @Override + public void send(SocketChannel channel, AwarenessMessage awareMsg) throws IOException { + /* OSC message args = [diagram name, siagram event action source]*/ + bundle = new CCmIOSCBundle(awareMsg.getTimestamp()); + Object[] args = new Object[2]; + args[0] = awareMsg.getDiagram(); + args[1] = awareMsg.getSource().toString(); + + bundle.addPacket(new OSCMessage( + OSC_NAME_PREFIX+awareMsg.getName().toString(), + args)); + try{ + writeBundle(channel); + }catch(IOException u){ + throw new IOException( + /* give a more user friendly message */ + ResourceBundle.getBundle(Server.class.getName()).getString("dialog.error.no_send"),u); + } + } + +// @Override +// public Command receiveCommand(SocketChannel channel) throws IOException { +// OSCBundle bundle = readBundle(channel); +// OSCMessage oscMessage = (OSCMessage)bundle.getPacket(0); +// String name = oscMessage.getName(); +// assert(name.startsWith(""+OSC_NAME_PREFIX)); +// name = name.substring(1); // chop off the trailing '/' +// Object args[] = new Object[oscMessage.getArgCount()-CMD_OFFSET]; +// for(int i=0; i< args.length;i++) +// args[i] = oscMessage.getArg(i+CMD_OFFSET); +// return new Command( +// Command.valueOf(name), +// (String)oscMessage.getArg(CMD_DIAGRAM_INDEX), +// args, +// bundle.getTimeTag(), +// DiagramEventSource.valueOf((String)oscMessage.getArg(CMD_SOURCE_INDEX)) +// ); +// } + + @Override + public Reply receiveReply(SocketChannel channel) throws IOException { + OSCBundle bundle = readBundle(channel); + OSCMessage oscMessage = (OSCMessage)bundle.getPacket(0); + String name = oscMessage.getName(); + assert(name.startsWith(""+OSC_NAME_PREFIX)); + name = name.substring(1); // chop off the trailing '/' + @SuppressWarnings("unused") + Integer len = (Integer)oscMessage.getArg(REPLY_LEN_INDEX); // of no use at the moment + return new Reply( + Reply.valueOf(name), + (String)oscMessage.getArg(REPLY_DIAGRAM_INDEX), + (String)oscMessage.getArg(REPLY_MESSAGE_INDEX), + bundle.getTimeTag(), + DiagramEventSource.valueOf((String)oscMessage.getArg(REPLY_SOURCE_INDEX))); + } + + @Override + public LockMessage receiveLockMessage(SocketChannel channel) throws IOException { + OSCBundle bundle = readBundle(channel); + OSCMessage oscMessage = (OSCMessage)bundle.getPacket(0); + String name = oscMessage.getName(); + name = name.substring(1); // chop off the trailing '/' + Object args[] = new Object[oscMessage.getArgCount()-LOCK_OFFSET]; + for(int i=0; i< args.length;i++) + args[i] = oscMessage.getArg(i+LOCK_OFFSET); + return new LockMessage( + LockMessage.valueOf(name), + bundle.getTimeTag(), + (String)oscMessage.getArg(LOCK_DIAGRAM_INDEX), + args, + DiagramEventActionSource.valueOf((String)oscMessage.getArg(LOCK_SOURCE_INDEX)) + ); + } + + public Message receiveMessage(SocketChannel channel) throws IOException { + OSCBundle bundle = readBundle(channel); + OSCMessage oscMessage = (OSCMessage)bundle.getPacket(0); + String name = oscMessage.getName(); + assert(name.startsWith(""+OSC_NAME_PREFIX)); + name = name.substring(1); // chop off the trailing '/' + if(name.endsWith(Reply.NAME_POSTFIX)){ // it's a reply + @SuppressWarnings("unused") + Integer len = (Integer)oscMessage.getArg(REPLY_LEN_INDEX); // of no use at the moment + Reply reply = new Reply( + Reply.valueOf(name), + (String)oscMessage.getArg(REPLY_DIAGRAM_INDEX), + (String)oscMessage.getArg(REPLY_MESSAGE_INDEX), + bundle.getTimeTag(), + DiagramEventSource.valueOf((String)oscMessage.getArg(REPLY_SOURCE_INDEX))); + return reply; + }else if (name.endsWith(LockMessage.NAME_POSTFIX)){ + Object args[] = new Object[oscMessage.getArgCount()-LOCK_OFFSET]; + for(int i=0; i< args.length;i++) + args[i] = oscMessage.getArg(i+LOCK_OFFSET); + return new LockMessage( + LockMessage.valueOf(name), + bundle.getTimeTag(), + (String)oscMessage.getArg(LOCK_DIAGRAM_INDEX), + args, + DiagramEventActionSource.valueOf((String)oscMessage.getArg(LOCK_SOURCE_INDEX)) + ); + }else if(name.endsWith(AwarenessMessage.NAME_POSTFIX)){ // it's an awareness message + AwarenessMessage.Name awName = AwarenessMessage.valueOf(name); + if(awName == AwarenessMessage.Name.USERNAME_A || awName == AwarenessMessage.Name.ERROR_A){ + return new AwarenessMessage(awName, + (String)oscMessage.getArg(AWAR_DIAGRAM_INDEX), + (String)oscMessage.getArg(AWAR_SOURCE_INDEX) + ); + }else { + return new AwarenessMessage(awName, + (String)oscMessage.getArg(AWAR_DIAGRAM_INDEX), + DiagramEventActionSource.valueOf((String)oscMessage.getArg(AWAR_SOURCE_INDEX)) + ); + } + }else{ // it's a command + Object args[] = new Object[oscMessage.getArgCount()-CMD_OFFSET]; + for(int i=0; i< args.length;i++) + args[i] = oscMessage.getArg(i+CMD_OFFSET); + return new Command( + Command.valueOf(name), + (String)oscMessage.getArg(CMD_DIAGRAM_INDEX), + args, + bundle.getTimeTag(), + DiagramEventSource.valueOf((String)oscMessage.getArg(CMD_SOURCE_INDEX)) + ); + } + } + + private OSCBundle readBundle(SocketChannel channel) throws IOException{ + /* read the size of the OSC packet, first 4 bytes according to OSC specs */ + buffer.rewind().limit(4); + while( buffer.hasRemaining() ) + if( channel.read( buffer ) == -1 ) + throw new SocketException(ResourceBundle.getBundle(Server.class.getName()).getString("error.connection_close")); + + buffer.rewind(); + int packetSize = buffer.getInt(); + assert(packetSize > 0 ); + ByteBuffer b = buffer; + /* if the packet is very big we must allocate an ad hoc temporary big big buffer */ + if(packetSize <= DEFAULT_CAPACITY) + b.rewind().limit(packetSize); + else + b = ByteBuffer.allocate(packetSize); + /* read the packet, it must be a bundle containing only one message */ + while( b.hasRemaining() ) + if( channel.read( b ) == -1 ) + throw new SocketException(ResourceBundle.getBundle(Server.class.getName()).getString("error.connection_close")); + b.rewind(); + return (OSCBundle)codec.decode(b); + } + + private void writeBundle(SocketChannel channel) throws IOException{ + ByteBuffer b = buffer; + buffer.clear(); + if(bundle.getSize() + 4 > DEFAULT_CAPACITY){ + b = ByteBuffer.allocate(bundle.getSize() + 4); + } + b.position(4); + bundle.encode(codec,b); + int len = b.position() - 4; + b.putInt(0, len); + b.flip(); + channel.write(b); + } + + ByteBuffer buffer; + CCmIOSCBundle bundle; + CCmIOSCPacketCodec codec; + + private static final int DEFAULT_CAPACITY = 1024; + private static final char OSC_NAME_PREFIX = '/'; + + + private static final int REPLY_LEN_INDEX = 0; + /* position of the diagram Name in the OSC message */ + private static final int REPLY_DIAGRAM_INDEX = 1; + private static final int REPLY_SOURCE_INDEX = 3; + private static final int CMD_DIAGRAM_INDEX = 0; + private static final int CMD_SOURCE_INDEX = 1; + private static final int CMD_OFFSET = 2; + private static final int LOCK_DIAGRAM_INDEX = 0; + private static final int LOCK_SOURCE_INDEX = 1; + private static final int LOCK_OFFSET = 2; + private static final int AWAR_DIAGRAM_INDEX = 0; + private static final int AWAR_SOURCE_INDEX = 1; + /* ------------------------------------------------*/ + private static final int REPLY_MESSAGE_INDEX = 2; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Protocol.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,45 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.io.IOException; +import java.nio.channels.SocketChannel; + +/** + * Objects implementing the {@code Protocol} interface will take care of + * the underlying communication protocol used over TCP/IP sockets. + * + */ +public interface Protocol { + void send(SocketChannel channel, Command cmd) throws IOException; + + void send(SocketChannel channel, Reply reply) throws IOException; + + void send(SocketChannel channel, LockMessage lock) throws IOException; + + void send(SocketChannel channel, AwarenessMessage awareMsg) throws IOException; + + Reply receiveReply(SocketChannel channel) throws IOException; + + LockMessage receiveLockMessage(SocketChannel channel) throws IOException; + + Message receiveMessage(SocketChannel channel) throws IOException; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ProtocolFactory.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,45 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The factory class to create {@code Protocol} instances. + * + */ +public abstract class ProtocolFactory { + public static Protocol newInstance(){ + return new OscProtocol(); + } + + /** + * Utility method to check if an address is in a valid IPv4 format. + * @param addr the address to check + * @return {@code true} if {@code addr} is in a valid IPv4 format + */ + public static boolean validateIPAddr(String addr){ + Matcher m = ip.matcher(addr); + return m.matches(); + } + + private static final Pattern ip = Pattern.compile("\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b"); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Reply.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,124 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource; +import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; + +/** + * A {@code Reply} message is sent from the server to a client from which it + * has just received a command. The reply acknowledges the client that the command has + * successfully been executed on the server. By broadcasting the command to the other client and by + * ending the reply to the client which issued the command itself the server keeps all the clients + * synchronized on its diagram model. + * + */ +public class Reply extends Message { + + public Reply(Name name, String diagram, String message, long timestamp, DiagramEventSource source){ + super(timestamp,diagram, source); + this.name = name; + this.message = message; + } + + public Reply(Name name, String diagram, String message, DiagramEventSource source){ + super(diagram,source); + this.name = name; + this.message = message; + } + + public Name getName() { + return name; + } + + public String getMessage() { + return message; + } + + @Override + public DiagramEventSource getSource(){ + return (DiagramEventSource)super.getSource(); + } + + /** + * @return the length of the String message conveyed by this Reply + */ + public int getMessageLen(){ + return message.length(); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + builder.append(timestamp).append(' ').append(name).append('\n').append(message); + return builder.toString(); + } + + public static Name valueOf(String n){ + Name name = Name.NONE_R; + try { + name = Name.valueOf(n); + }catch(IllegalArgumentException iae){ + iae.printStackTrace(); + } + return name; + } + + public static void log(Reply reply){ + if(reply.getName() != Reply.Name.TRANSLATE_EDGE_R && reply.getName() != Reply.Name.TRANSLATE_NODE_R && reply.getName() != Reply.Name.BEND_R){ + StringBuilder builder = new StringBuilder(reply.getName().toString()); + builder.append(' ').append(reply.getDiagram()); + builder.append(' ').append(reply.getMessage()); + InteractionLog.log("SERVER", "reply received", builder.toString()); + } + } + + private Name name; + private String message; + private long timestamp; + public static final String NAME_POSTFIX = "_R"; + public static enum Name implements Message.MessageName { + NONE_R, + OK_R, + ERROR_R, + LIST_R, + GET_R, + INSERT_NODE_R, + REMOVE_NODE_R, + INSERT_EDGE_R, + REMOVE_EDGE_R, + SET_NODE_NAME_R, + SET_EDGE_NAME_R, + SET_PROPERTY_R, + SET_PROPERTIES_R, + CLEAR_PROPERTIES_R, + SET_NOTES_R, + ADD_PROPERTY_R, + REMOVE_PROPERTY_R, + SET_MODIFIERS_R, + SET_ENDDESCRIPTION_R, + SET_ENDLABEL_R, + TRANSLATE_NODE_R, + TRANSLATE_EDGE_R, + BEND_R, + STOP_EDGE_MOVE_R, + STOP_NODE_MOVE_R; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Server.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,225 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Queue; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.utils.PreferencesService; + +/** + * The {@code Server} is a thread the user can start, which accept connections from other clients + * (other users machines running the CCmI editor). + * When a user shares a diagram via the server, other clients + * will be able to see and to modify it in real time. This is achieved by exchanging network + * message between the server and each client. + * On a shared diagram scenario the server + * diagram model is considered to be the "real" one. That is, the other clients will continuously + * synchronize their model with the server and even before applying a command issued by an action of + * the local user, clients need first send the command to the server (which will apply the command on its own model) + * and wait for an acknowledging reply. + * + */ +public class Server extends NetworkThread { + + /** + * Create a new server instance. If another instance is already running then it's shut + * down before the new instance is created. + * + * @return a {@code Server} instance. + */ + public static Server createServer(){ + if(server != null) + server.shutdown(server.resources.getString("log.restart")); + server = new Server(); + return server; + } + + public static Server getServer(){ + return server; + } + + private Server(){ + super("Server Thread"); + mustSayGoodbye = false; + initialized = false; + resources = ResourceBundle.getBundle(this.getClass().getName()); + } + + public void init() throws IOException { + if(initialized) + return; + serverChannel = ServerSocketChannel.open(); + selector = Selector.open(); + diagrams = new HashMap<String,Diagram>(); + 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); + 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; + } + + @Override + public void run(){ + logger.config(resources.getString("log.start")); + running = true; + while(!mustSayGoodbye){ + try{ + selector.select(); + if(mustSayGoodbye) + break; + for (Iterator<SelectionKey> itr = selector.selectedKeys().iterator(); itr.hasNext();){ + SelectionKey key = itr.next(); + itr.remove(); + + if(!key.isValid()) + continue; + if(key.isAcceptable()){ + SocketChannel clientChannel = serverChannel.accept(); + clientChannel.configureBlocking(false); + clientChannel.register(selector, SelectionKey.OP_READ); + /* 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{ + scManager.handleMessage((SocketChannel)key.channel()); + }catch(IOException ioe){ + /* Upon exception the channel is no longer considered reliable: it's cleaned up and closed */ + SocketChannel channel = (SocketChannel)key.channel(); + channel.close(); + scManager.removeChannel(channel); + logger.info(ioe.getLocalizedMessage()); + } + } + } + }catch(IOException e){ + logger.severe(e.getLocalizedMessage()); + e.printStackTrace(); + break; + } + } + cleanup(); + } + + private void cleanup(){ + try { + /* close all the channels */ + serverChannel.close(); + for (Iterator<SelectionKey> itr = selector.keys().iterator(); itr.hasNext();){ + SelectionKey key = itr.next(); + ((Closeable)key.channel()).close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + running = false; + } + + public void shutdown(String msg){ + if(msg != null) + NarratorFactory.getInstance().speak(resources.getString("log.shutdown")+msg); + shutdown(); + } + + public void shutdown(){ + mustSayGoodbye = true; + selector.wakeup(); + server = null; + for(Handler h : logger.getHandlers()){ + h.close(); + logger.removeHandler(h); + } + } + + public Queue<DiagramElement> getLocalhostQueue(String diagramName){ + return scManager.getLocalhostMap().get(diagramName); + } + + /* this is called by the event dispatching thread when the user shares a diagram */ + public void share(Diagram diagram) throws ServerNotRunningException, DiagramAlreadySharedException{ + String name = diagram.getName(); + if(!running) + throw new ServerNotRunningException(resources.getString("exception_msg.not_run")); + synchronized(diagrams){ + if(diagrams.containsKey(name)){ + throw new DiagramAlreadySharedException(resources.getString("exception_msg.already_shared")); + } + diagrams.put(name,diagram); + } + scManager.getLocalhostMap().put(name, new ConcurrentLinkedQueue<DiagramElement>()); + logger.info("Diagram "+name+" shared on the server"); + } + + private static Server server; + public static final String DEFAULT_LOCAL_PORT = "7777"; + public static final String DEFAULT_REMOTE_PORT = "7777"; + static Logger logger; + private ServerSocketChannel serverChannel; + private ServerConnectionManager scManager; + private Selector selector; + private Map<String, Diagram> diagrams; + private ResourceBundle resources; + private boolean initialized; + private volatile boolean mustSayGoodbye; + private boolean running; + + static { + logger = Logger.getLogger(Server.class.getName()); + logger.setUseParentHandlers(false); + logger.setLevel(Level.CONFIG); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/Server.properties Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,15 @@ + +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 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ServerConnectionManager.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,822 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.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.DiagramElement; +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.pdsupport.PdDiagram; +import uk.ac.qmul.eecs.ccmi.pdsupport.PdPersistenceManager; +import uk.ac.qmul.eecs.ccmi.speech.Narrator; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; + +/* This class manages the different sessions with the clients. whereas the + * Server class just listens for connections, this class manages all what's going on about the + * diagram editing: e.g. command processing consistency check, client updates etc. + */ +class ServerConnectionManager { + ServerConnectionManager( Map<String,Diagram> diagrams, AwarenessPanelEditor panelEditor) { + this.diagrams = diagrams; + awarenessPanelEditor = panelEditor; + diagramChannelAllocations = new HashMap<Diagram,Set<UserNameSocketChannel>>(); + localhostDiagramElementQueue = new ConcurrentHashMap<String,Queue<DiagramElement>>(); + protocol = ProtocolFactory.newInstance(); + lockManager = new ServerLockManager(); + broadcastFilter = BroadcastFilter.getInstance(); + } + + /** + * Removes the channel and the locks related to it from the inner data structures. + * + * @param channel the channel to remove + */ + void removeChannel(SocketChannel channel) throws IOException{ + String diagramName = null; + /* looks for the Set containing this channel */ + for(Map.Entry<Diagram,Set<UserNameSocketChannel>> entry : diagramChannelAllocations.entrySet()){ + UserNameSocketChannel unsc = null; + for(UserNameSocketChannel userNameSocketChannel : entry.getValue()) + if(userNameSocketChannel.channel.equals(channel)){ + unsc = userNameSocketChannel; + break; + } + /* remove the channel from this set of channels */ + if(entry.getValue().remove(unsc) && !unsc.userName.isEmpty()){ + diagramName = entry.getKey().getName(); + awarenessPanelEditor.removeUserName(diagramName, unsc.userName); + /* notify the other clients the user has disconnected */ + AwarenessMessage awMsg = new AwarenessMessage( + AwarenessMessage.Name.USERNAME_A, + diagramName, + ""+AwarenessMessage.USERNAMES_SEPARATOR+unsc.userName + ); + for(UserNameSocketChannel userNameSocketChannel : entry.getValue()){ + /* don't send aw msg to the local channel */ + if(!userNameSocketChannel.channel.equals(localChannel)) + protocol.send(userNameSocketChannel.channel, awMsg); + } + break; + } + + } + /* all locks held by disconnected user are released */ + lockManager.removeLocks(channel, diagramName); + } + + void handleMessage(SocketChannel channel) throws IOException{ + Message message = protocol.receiveMessage(channel); + if(message instanceof Command){ + handleCommand((Command)message, channel); + }else if(message instanceof LockMessage) { + handleLockMessage((LockMessage)message,channel); + }else { // awareness message - broadcast the message + handleAwarenessMessage((AwarenessMessage)message,channel,null); + } + } + + private void handleLockMessage(LockMessage lockMessage, SocketChannel channel) throws IOException{ + Lock lock = LockMessageConverter.getLockFromMessageName((LockMessage.Name)lockMessage.getName()); + String name = lockMessage.getName().toString(); + String diagramName = lockMessage.getDiagram(); + + 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); + } + + 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; + } + //System.out.println("Lock message received: " + name +" diagram:"+diagramName+" treenode:"+treeNode.getName() ); + + /* check whether it's a GET or YIELD message and act accordingly */ + if(name.startsWith(LockMessage.GET_LOCK_PREFIX)){ + // System.out.println("get lock source:"+ lockMessage.getSource()); + boolean succeeded; + succeeded = lockManager.requestLock(treeNode, lock, channel, diagramName); + monitor.unlock(); + /* send the response */ + replyLockMessage(channel,diagramName,succeeded); + if(succeeded && broadcastFilter.accept(lockMessage.getSource())){ + DiagramEventActionSource processedSource = broadcastFilter.process(lockMessage.getSource()); // changes according to configuration; + + //select node is a temporary record, therefore it doesn't need to be stored + if(processedSource.getCmd() != Command.Name.SELECT_NODE_FOR_EDGE_CREATION){ + Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram); + /* saves the diagramEventActionSource for when the lock is yielded */ + if(userNames != null){ + for(UserNameSocketChannel userName : userNames){ + if(userName.channel.equals(channel)){ + userName.lockAwarenessSources.add(processedSource); + } + } + } + } + /* handle the awareness message piggybacked in the lock message */ + AwarenessMessage awarMsg = new AwarenessMessage( + AwarenessMessage.Name.START_A, + diagramName, + processedSource + ); + handleAwarenessMessage(awarMsg,channel,diagram); + } + }else{ // yield lock + 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 && broadcastFilter.accept(source)){ + /* 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(); + 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 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 : // the local socket makes itself known to the server + localChannel = channel; + Set<UserNameSocketChannel> list = new HashSet<UserNameSocketChannel>(); + list.add(new UserNameSocketChannel(localChannel,AwarenessMessage.getDefaultUserName())); + diagramChannelAllocations.put(diagram, list); + broadcast = false; + break; + 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(),DiagramEventSource.NONE)); + broadcast = false; + break; + case GET : // ask for a diagram xml + try{ + diagram.getCollectionModel().getMonitor().lock(); + Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + if(diagram instanceof PdDiagram){ + PdPersistenceManager.getInstance().encodeDiagramInstance(diagram, new BufferedOutputStream(out)); + }else{ + 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"),DiagramEventSource.NONE)); + for(UserNameSocketChannel sc: userNames){ + protocol.send(channel, new AwarenessMessage(AwarenessMessage.Name.USERNAME_A,diagramName,sc.userName)); + } + }catch(IOException ioe){ + throw ioe; + } + userNames.add(new UserNameSocketChannel(channel)); + broadcast = false; + }catch(Exception e){ + // close the socket, log and discard the packet + try{channel.close();}catch(IOException ioe){ioe.printStackTrace();} + Server.logger.severe(e.getMessage()); + } + break; + case INSERT_NODE : + if(channel == localChannel){ + /* if the command is coming from the local user then there is * + * a diagram element queued, which is the one the user wanted to insert */ + node = (Node)localhostDiagramElementQueue.get(diagramName).poll(); + }else{ + double dx = (Double)cmd.getArgAt(1); + double dy = (Double)cmd.getArgAt(2); + 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); + } + } + } + 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{ + 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 { + 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",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{ + edge = Finder.findEdge((String)cmd.getArgAt(0),diagram.getEdgePrototypes()); + edge = (Edge)edge.clone(); + nodesToConnect = new long[cmd.getArgNum()-1]; + for(int i=0;i<nodesToConnect.length;i++){ + nodesToConnect[i] = (Long)cmd.getArgAt(i+1); + } + } + /* wait for the edge to be inserted in the model so that it gets an id, which is then * + * used in the awareness message to notify other users that the node has been inserted */ + try { + SwingUtilities.invokeAndWait(new CommandExecutor.Insert(diagram.getCollectionModel(), edge, nodesToConnect, source)); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + /* send the reply to the client which issued the command */ + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.INSERT_EDGE_R,diagramName,"Insert new Edge", source.getLocalSource())); + + /* send the awareness message for edge insertion */ + DiagramEventActionSource actSource = new DiagramEventActionSource(source, + Command.Name.INSERT_EDGE,edge.getId(),edge.getName()); + if(broadcastFilter.accept(actSource)){ + /* must set the username to the one of the client who sent the command * + * otherwise the local username is automatically assigned in the constructor */ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel.equals(channel)) + actSource.setUserName(sc.userName); + } + + /* process it with the broadcast filter */ + DiagramEventActionSource processedSource = broadcastFilter.process(actSource); + + /* since no lock is used we must send an awareness message without piggybacking */ + AwarenessMessage awMsg = new AwarenessMessage( + AwarenessMessage.Name.START_A, + diagramName, + processedSource + ); + + if(channel != localChannel){ + /* update the local awareness panel and speech */ + awarenessPanelEditor.addTimedRecord(awMsg.getDiagram(), DisplayFilter.getInstance().processForText(processedSource)); + NarratorFactory.getInstance().speakWholeText( + DisplayFilter.getInstance().processForSpeech(processedSource), Narrator.SECOND_VOICE); + } + /* broadcast the awareness message to all the clients but one which sent the * + * command and the local one to inform them the action has started */ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(!sc.channel.equals(channel) && !sc.channel.equals(localChannel)){ + protocol.send(sc.channel, awMsg); + } + } + } + break; + case REMOVE_EDGE : + if(channel == localChannel){ + /* if the command is coming from the local user then there is a diagram */ + /* element queued, which is the one the user wanted to insert */ + edge = (Edge)localhostDiagramElementQueue.get(diagramName).poll(); + }else{ + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + } + /* wait for the Event Dispatching Thread to delete the edge, in order to avoid collisions * + * with other locks, e.g. locking again an edge before the EDT deletes it */ + try { + lockManager.removeLocks(edge, diagramName); + SwingUtilities.invokeAndWait(new CommandExecutor.Remove(diagram.getCollectionModel(), edge, source)); + } catch (Exception e) { + throw new RuntimeException(e); // must never happen + } + /* remove the action source, like when a lock is yielded, for this node */ + removeEventActionSource(channel,edge.getId(),diagram); + /* send the reply to the client which issued the command */ + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.REMOVE_EDGE_R,diagramName,"Edge with id "+ edge.getId() +" removed", source.getLocalSource())); + break; + case SET_EDGE_NAME : { + DiagramElement de = null; + diagram.getCollectionModel().getMonitor().lock(); + de = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetName(de,((String)cmd.getArgAt(1)),source)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_EDGE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1),source.getLocalSource())); + }break; + case SET_NODE_NAME : { + DiagramElement de = null; + diagram.getCollectionModel().getMonitor().lock(); + de = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetName(de,((String)cmd.getArgAt(1)),source)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_NODE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1),source.getLocalSource())); + }break; + case SET_PROPERTY : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetProperty( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + (String)cmd.getArgAt(3), + source + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(2)+ " set to "+ cmd.getArgAt(3),source.getLocalSource())); + break; + case SET_PROPERTIES : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + + SwingUtilities.invokeLater(new CommandExecutor.SetProperties( + node, + (String)cmd.getArgAt(1), + source + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_PROPERTIES_R,diagramName,"Properties for " + node.getName()+ " set to "+ cmd.getArgAt(1),source.getLocalSource())); + break; + case CLEAR_PROPERTIES : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node,source)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.CLEAR_PROPERTIES_R,diagramName,"Propertis of Node "+ node.getName() +" cleared",source.getLocalSource())); + break; + case SET_NOTES :{ + DiagramTreeNode treeNode = null; + int[] path = new int[cmd.getArgNum()-1]; + for(int i = 0; i< cmd.getArgNum()-1;i++){ + path[i] = (Integer)cmd.getArgAt(i); + } + final String notes = (String)cmd.getArgAt(cmd.getArgNum()-1); + diagram.getCollectionModel().getMonitor().lock(); + treeNode = Finder.findTreeNode(path, (DiagramTreeNode)diagram.getTreeModel().getRoot()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetNotes(diagram.getTreeModel(),treeNode,notes,source)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_NOTES_R,diagramName,"Notes for " + treeNode.getName() + " successfully updated",source.getLocalSource())); + }break; + case ADD_PROPERTY : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.AddProperty( + node, + (String)cmd.getArgAt(1), + (String)cmd.getArgAt(2), + source + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.ADD_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " added to "+ cmd.getArgAt(1),source.getLocalSource())); + break; + case REMOVE_PROPERTY : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + source)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.REMOVE_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " of type "+ cmd.getArgAt(1)+" removed",source.getLocalSource())); + break; + case SET_MODIFIERS : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + indexes = new HashSet<Integer>(cmd.getArgNum()-3); + for(int i=3;i<cmd.getArgNum();i++){ + indexes.add((Integer)cmd.getArgAt(i)); + } + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetModifiers( + node, + (String)cmd.getArgAt(1), + (Integer)cmd.getArgAt(2), + indexes, + source)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_MODIFIERS_R,diagramName,"Modifiers for " + cmd.getArgAt(1)+ " successfully set",source.getLocalSource())); + break; + case SET_ENDLABEL : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel(edge,node,(String)cmd.getArgAt(2),source)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_ENDLABEL_R,diagramName,"Endlabel set to "+ cmd.getArgAt(2) +" for edge "+edge.getName(),source.getLocalSource())); + break; + case SET_ENDDESCRIPTION : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription( + edge, + node, + (Integer)cmd.getArgAt(2), + source + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.SET_ENDDESCRIPTION_R,diagramName,"End description set to " + +(cmd.getArgNum() == 3 ? cmd.getArgAt(2) : Edge.NO_ENDDESCRIPTION_STRING) + + " for edge",source.getLocalSource())); + break; + case TRANSLATE_NODE : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.Translate( + node, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + (Double)cmd.getArgAt(3), + (Double)cmd.getArgAt(4), + source + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.TRANSLATE_NODE_R,diagramName,"Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")",source.getLocalSource())); + break; + case TRANSLATE_EDGE : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.Translate( + edge, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + (Double)cmd.getArgAt(3), + (Double)cmd.getArgAt(4), + source + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.TRANSLATE_EDGE_R, + diagramName, + "Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")", + source.getLocalSource()) + ); + break; + case BEND : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + Point2D bendStart = null; + if(cmd.getArgNum() == 5){ + bendStart = new Point2D.Double((Double)cmd.getArgAt(3),(Double)cmd.getArgAt(4)); + } + SwingUtilities.invokeLater(new CommandExecutor.Bend( + edge, + new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)), + bendStart, + source + )); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.BEND_R,diagramName,"Bend at point: ("+(Double)cmd.getArgAt(1)+","+(Double)cmd.getArgAt(1)+")",cmd.getSource().getLocalSource())); + break; + case STOP_EDGE_MOVE : + diagram.getCollectionModel().getMonitor().lock(); + edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge,source)); + if(channel != localChannel) + protocol.send(channel, new Reply(Reply.Name.STOP_EDGE_MOVE_R,diagramName,"Undo straight bends",source.getLocalSource())); + break; + case STOP_NODE_MOVE : + diagram.getCollectionModel().getMonitor().lock(); + node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes()); + diagram.getCollectionModel().getMonitor().unlock(); + SwingUtilities.invokeLater(new CommandExecutor.StopMove(node,source)); + if(channel != localChannel){ + protocol.send(channel, new Reply(Reply.Name.STOP_NODE_MOVE_R,diagramName,"Stop node move",source.getLocalSource())); + } + break; + default : throw new RuntimeException(cmd.getName().toString()+ " command not recognized"); + } + if(broadcast){ + /* broadcast the command to all the clients but the local (uses the same model) and the one which issued the command (got a reply already)*/ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel != localChannel && !sc.channel.equals(channel)){ + protocol.send(sc.channel, cmd); + } + } + } + } + + private void handleAwarenessMessage(AwarenessMessage awMsg,SocketChannel channel, Diagram diagram) throws IOException { + if(diagram == null) + synchronized(diagrams){ + diagram = diagrams.get(awMsg.getDiagram()); + } + + if(awMsg.getName() == AwarenessMessage.Name.ERROR_A){ + Logger.getLogger(Server.class.getCanonicalName()).info((String)awMsg.getSource()); + return; + } + + /* for username aw msg checks whether the chosen name is not already used by another client. * + * If not changes the source from "newName", sent by the client, into "newName<SEPARATOR>oldName" * + * in order to broadcast it to the other client and make them replace the new name with the old one */ + String oldName = ""; + if(awMsg.getName() == AwarenessMessage.Name.USERNAME_A){ + String userName = (String)awMsg.getSource(); + UserNameSocketChannel userNameChannel = null; + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel.equals(channel)){ + userNameChannel = sc; + oldName = userNameChannel.userName; + + } + /* if another user already has the name then prevent from getting it */ + if(sc.userName.equals(userName)){ + /* user name already in use, send a reply and return */ + protocol.send(channel, new AwarenessMessage( + AwarenessMessage.Name.ERROR_A, + awMsg.getDiagram(), + ResourceBundle.getBundle(Server.class.getName()).getString("awareness.msg.user_already_exists") + )); + return; + } + } + userNameChannel.userName = userName; + /* set the source of the msg for the clients, which don't hold the channel-username association * + * and therefore need a message of the form "newName<SEPARATOR>oldName" in order to do the replacement */ + awMsg.setSource((String)awMsg.getSource()+AwarenessMessage.USERNAMES_SEPARATOR+oldName); + } + + /* update the local GUI to make the local user aware of the actions */ + DisplayFilter filter = DisplayFilter.getInstance(); + if(channel != localChannel && filter != null){ + if(awMsg.getName() == AwarenessMessage.Name.USERNAME_A){ + awarenessPanelEditor.replaceUserName(awMsg.getDiagram(), (String)awMsg.getSource()); + }else{ + DiagramEventActionSource processedSource = broadcastFilter.process((DiagramEventActionSource)awMsg.getSource()); // changes according to configuration; + if(filter.configurationHasChanged()){ + for(Diagram d : diagramChannelAllocations.keySet()) + awarenessPanelEditor.clearRecords(d.getName()); + } + + /* select and unselect are announced and written (temporary) on the panel, regardless START_A and STOP_A */ + if(processedSource.getCmd() == Command.Name.SELECT_NODE_FOR_EDGE_CREATION || processedSource.getCmd() == Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION){ + awarenessPanelEditor.addTimedRecord(awMsg.getDiagram(), filter.processForText(processedSource)); + NarratorFactory.getInstance().speakWholeText(filter.processForSpeech(processedSource), Narrator.SECOND_VOICE); + }else if(awMsg.getName() == AwarenessMessage.Name.START_A){ + awarenessPanelEditor.addRecord(awMsg.getDiagram(), filter.processForText(processedSource)); + /* announce the just received awareness message via the second voice */ + NarratorFactory.getInstance().speakWholeText(filter.processForSpeech(processedSource), Narrator.SECOND_VOICE); + }else{ // STOP_A + /* selection is a timedRecord, therefore no need to remove the record on STOP_A */ + if(processedSource.getCmd() != Command.Name.SELECT_NODE_FOR_EDGE_CREATION && processedSource.getCmd() != Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION) + awarenessPanelEditor.removeRecord(awMsg.getDiagram(), filter.processForText(processedSource)); + } + } + } + + /* broadcast the awareness message to all the clients but the local and * + * one which sent it, to inform them the action has started */ + for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){ + if(sc.channel != localChannel && !sc.channel.equals(channel)) + protocol.send(sc.channel, awMsg); + } + } + + Map<String, Queue<DiagramElement>> getLocalhostMap() { + return localhostDiagramElementQueue; + } + + private void replyLockMessage(SocketChannel channel, String diagramName,boolean yes) throws IOException{ + protocol.send(channel, new LockMessage( + yes ? LockMessage.Name.YES_L : LockMessage.Name.NO_L, + diagramName, + -1, + DiagramEventActionSource.NULL + )); + } + + private DiagramEventActionSource removeEventActionSource(SocketChannel channel, long saveID, Diagram diagram){ + Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram); + if(userNames != null){ + for(UserNameSocketChannel userName : userNames){ + if(userName.channel.equals(channel)){ + for(DiagramEventActionSource s : userName.lockAwarenessSources){ + if(s.getSaveID() == saveID){ + userName.lockAwarenessSources.remove(s); + return s; + } + } + } + } + } + return null; + } + + private Set<Integer> indexes; + /* the String key is the name of diagram, this collection is shared with the class Server * + * and it's used to retrieve the diagrams out of commands */ + private Map<String,Diagram> diagrams; + /* unique localChannel for all the digrams */ + private SocketChannel localChannel; + /* this map contains all the channels bound to a diagram, so if a change * + * is made to a diagram all its channels are broadcasted through this map */ + private Map<Diagram,Set<UserNameSocketChannel>> diagramChannelAllocations; + /* this map is used to pass the reference to elements created by the local client * + * (we don't create a new object as well as we do for the nodes created by remote clients */ + private Map<String, Queue<DiagramElement>> localhostDiagramElementQueue; + private Protocol protocol; + private ServerLockManager lockManager; + private BroadcastFilter broadcastFilter; + private AwarenessPanelEditor awarenessPanelEditor; + + /* this class holds for each socketChannel the username associated to it + * and the last received awareness message source,*/ + private static class UserNameSocketChannel { + UserNameSocketChannel(SocketChannel channel){ + this(channel,""); + } + + UserNameSocketChannel(SocketChannel channel, String userName){ + this.channel = channel; + this.userName = userName; + lockAwarenessSources = new LinkedList<DiagramEventActionSource>(); + } + + SocketChannel channel; + String userName; + List<DiagramEventActionSource> lockAwarenessSources; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ServerLockManager.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,385 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +import java.nio.channels.SocketChannel; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.swing.tree.TreeNode; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement; +import uk.ac.qmul.eecs.ccmi.diagrammodel.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; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Lock; +import uk.ac.qmul.eecs.ccmi.gui.Node; + +/** + * + * This class keeps track of the objects currently locked by the users. + * Locking is done by inserting a lock entry into a list of entries. All lock types have one or more dependencies. + * A dependency is a lock on the same or another tree node, which prevent the lock from being acquired. + * The dependency lock can also be of another type of the lock we're trying to acquire. + * For example if I want to change the arrow head of edge E, before inserting a arrow-head lock in the list + * I must be sure nobody else has already entered a delete-lock for E, as it means that user is about to + * delete the edge I want to change the arrow of. Likewise if I want to delete a node I must be sure + * nobody else inserted a note-lock on a edge reference laying within that node in the tree as that edge reference + * will go after the node is deleted. If a user tries to acquire a lock and one or more dependencies + * have been already inserted in the list by other users then the lock won't be granted. I no dependency + * exist in the list the lock entry is created and inserted. + * + * + * Lock dependencies (unless differently specified the dependency lock is on the same tree node): + * DELETE : DELETE,ARROW_HEAD,END_LABEL,MOVE,MUST_EXIST,NAME,NOTES, BOOKMARK,PROPERTIES, + * DELETE on two ended edges if this is a node, NOTES and BOOKMARK on those tree node + * that will be deleted as a result of this action. + * EDGE_END : EDGE_END, DELETE + * MOVE : MOVE, DELETE + * MUST_EXIST : DELETE + * NAME : NAME, DELETE + * NOTES : NOTES, DELETE, DELETE on Objects whose deletion would entail the deletion of this tree node + * PROPERTIES if this is a subtree of a property node. + * BOOKMARK : DELETE + * PROPERTIES : PROPERTIES, DELETE + * + * + */ +class ServerLockManager { + public ServerLockManager(){ + locksMap = new HashMap<String,List<LockEntry>>(); + } + + /* check if the specified lock is present in the lock list for the specified tree node */ + private boolean lockExists(DiagramTreeNode treeNode, Lock lock, List<LockEntry> locks, SocketChannel channel){ + for(LockEntry lockEntry : locks){ + if(lockEntry.treeNode.equals(treeNode) && lockEntry.lock == lock && !lockEntry.channel.equals(channel)) + return true; + } + return false; + } + + /* check if either specified locks is present in the lock list for the specified tree node */ + private boolean lockExists(DiagramTreeNode treeNode, Lock lock1, Lock lock2, List<LockEntry> locks, SocketChannel channel){ + for(LockEntry lockEntry : locks){ + if(lockEntry.treeNode.equals(treeNode) && (lockEntry.lock == lock1 || lockEntry.lock == lock2) && !lockEntry.channel.equals(channel) ) + return true; + } + return false; + } + + /* Check whether the lock can be granted as it does */ + /* not clash with other locks owned by other clients */ + private boolean checkLockDependencies(DiagramTreeNode treeNode, Lock lock, SocketChannel channel, List<LockEntry> locks){ + /* bookmarks are not shared, we only check them against delete-lock, as editing a */ + /* bookmark on a tree node that has just been would lead to an inconsistent state */ + if(lock != Lock.BOOKMARK) + for(LockEntry lockEntry : locks){ + /* if the two elements are different, there is no possible clash. Go ahead */ + if(!lockEntry.treeNode.equals(treeNode)) + continue; + /* locks are reentrant, that is if a client requests a lock he's already got * + * then he's automatically granted for that lock */ + if(lockEntry.channel.equals(channel)){ + if(lock.equals(lockEntry.lock)){ + return true; + } + continue; //if the clients has ad different lock then just go ahead + } + /* DELETE depends on all the other locks and all the other locks depend on it */ + /* (bear in mind that at this point of the code lockEntry.treeNode == treeNode) */ + if(lock == Lock.DELETE||lockEntry.lock == Lock.DELETE) + return false; + /* unless the lock is of type MUST_EXIST, if someone else has the desired lock, then the client */ + /* cannot get it that is, only MUST_EXIST can be shared by different clients at the same time */ + if(lock != Lock.MUST_EXIST && lock == lockEntry.lock) + return false; + } + + /* delete-locks on nodes and edges involved with treeNode will prevent from acquiring the lock as treeNode might */ + /* no longer exist after the editing. See the following comments to figure out what "involved" means in this context */ + if(lock == Lock.NOTES || lock == Lock.BOOKMARK){ + /* no diagram element along the path from root to this treeNode must be delete-locked or properties-locked */ + /* (as editing a property might mean delete some tree nodes and, maybe, this tree node) */ + for(TreeNode tn : treeNode.getPath()){ + if(tn instanceof DiagramElement) + if(lockExists((DiagramElement)tn,Lock.DELETE,Lock.PROPERTIES,locks,channel)) + return false; + } + /* if note-locking a reference tree node, the referred diagram element must not be delete-locked */ + /* as the reference will go as well after the diagram element will be deleted */ + if(treeNode instanceof NodeReferenceMutableTreeNode){ + NodeReferenceMutableTreeNode referenceNode = (NodeReferenceMutableTreeNode)treeNode; + if(lockExists(referenceNode.getNode(),Lock.DELETE,locks,channel)) + return false; + } + if(treeNode instanceof EdgeReferenceMutableTreeNode){ + EdgeReferenceMutableTreeNode referenceNode = (EdgeReferenceMutableTreeNode)treeNode; + if(lockExists(referenceNode.getEdge(),Lock.DELETE,locks,channel)) + return false; + } + /* if note locking an edge reference tree holder which has only one child, we cannot grant * + * the lock if the referred edge is delete-locked as the holder will be deleted as well after * + * the eventual deletion of the edge */ + if(treeNode instanceof EdgeReferenceHolderMutableTreeNode && treeNode.getChildCount() == 1){ + EdgeReferenceMutableTreeNode referenceNode = (EdgeReferenceMutableTreeNode)treeNode.getChildAt(0); + if(lockExists(referenceNode.getEdge(),Lock.DELETE,locks,channel)) + return false; + } + } + + if(lock == Lock.DELETE){ + /* all the descendants of the element must be non notes-locked or bookmark-locked */ + for(@SuppressWarnings("rawtypes") + Enumeration enumeration = treeNode.breadthFirstEnumeration(); enumeration.hasMoreElements();){ + if(lockExists((DiagramTreeNode)enumeration.nextElement(),Lock.NOTES,Lock.BOOKMARK,locks,channel)) + return false; + } + + if(treeNode instanceof Node){ + Node n = (Node)treeNode; + /* if we want to delete a Node we must get the lock on each attached * + * edge as they will be deleted as well */ + for(int i =0; i< n.getEdgesNum();i++){ + Edge e = n.getEdgeAt(i); + if(lockExists(e,Lock.DELETE,locks,channel)) + return false; + /* In order to delete-lock a Node, no referee must be bookmark/notes-locked. The referees * + * are the NodeReferenceTreeNode's pointing to this Node */ + for(int j=0;j<e.getChildCount();j++){ + NodeReferenceMutableTreeNode nodeRef = (NodeReferenceMutableTreeNode)e.getChildAt(j); + if(nodeRef.getNode().equals(n)) + if(lockExists(nodeRef,Lock.NOTES,Lock.BOOKMARK,locks,channel)) + return false; + } + } + } + + if(treeNode instanceof Edge){ + /* for each node check whether the reference to this edge is notes-locked */ + Edge e = (Edge)treeNode; + for(int i=0;i<e.getNodesNum();i++){ + Node n = e.getNodeAt(i); + for(int j=0;j<n.getChildCount();j++){ + if(n.getChildAt(j) instanceof EdgeReferenceHolderMutableTreeNode){ + EdgeReferenceHolderMutableTreeNode refHolder = (EdgeReferenceHolderMutableTreeNode)n.getChildAt(j); + /* someone else is editing notes on the reference holder and it has only one child */ + /* which means it will be deleted after the edge deletion, the lock cannot be granted then */ + if((refHolder.getChildCount() == 1) && lockExists(refHolder,Lock.NOTES,Lock.BOOKMARK,locks,channel)) + return false; + /* if a reference tree node pointing to this edge is notes-locked, the edge can't be deleted */ + for(int k=0;k<refHolder.getChildCount();k++){ + EdgeReferenceMutableTreeNode edgeRef = (EdgeReferenceMutableTreeNode)refHolder.getChildAt(k); + if(lockExists(edgeRef,Lock.NOTES,Lock.BOOKMARK,locks,channel)) + return false; + } + } + } + } + } + } + + /* all the checks have been passed, the client definitely deserves the lock now */ + return true; + } + + /** + * Request an editing lock for a tree node + * + * @param treeNode the treeNode the caller is trying to lock + * @param lock the type of lock requested + * @param channel the channel works as a unique identifier for the clients + * @param diagramName the name of the diagram the lock is requested on + * @return true if the lock is successfully granted, or false otherwise (because of another client + * holding a lock which clashes with this request) + */ + public boolean requestLock(DiagramTreeNode treeNode, Lock lock, SocketChannel channel,String diagramName){ +// System.out.println("lock before request:"+lockStatusDescription(diagramName)+"\n----"); + List<LockEntry> locks = locksMap.get(diagramName); + if(locks == null){ + /* if no object in the diagram has ever been locked */ + /* there is no entry in the map and one must be created */ + locks = new LinkedList<LockEntry>(); + locksMap.put(diagramName,locks); + } + /* deleting a node will cause all the attached two-ended edges to * + * be deleted, therefore we need to lock all those edges too, before */ + if(lock == Lock.DELETE && treeNode instanceof Node){ + Node n = (Node)treeNode; + for(int i=0; i<n.getEdgesNum();i++){ + if(n.getEdgeAt(i).getNodesNum() > 2) + continue; + boolean succeeded = requestLock(n.getEdgeAt(i),Lock.DELETE,channel,diagramName); + if(!succeeded){ + /* release the previously acquired locks and return a failure */ + for(int j=0;j<i;j++){ + if(n.getEdgeAt(j).getNodesNum() == 2) + releaseLock(n.getEdgeAt(j),Lock.DELETE,channel,diagramName); + } + return false; + } + } + } + + if(!checkLockDependencies(treeNode,lock,channel,locks)){ + if(lock == Lock.DELETE && treeNode instanceof Node){ + Node n = (Node)treeNode; + for(int j=0;j<n.getEdgesNum();j++){ + if(n.getEdgeAt(j).getNodesNum() == 2) + releaseLock(n.getEdgeAt(j),Lock.DELETE,channel,diagramName); + } + } + return false; + } + + /* adds the lock only if it doesn't already exist */ + boolean add = true; + for(LockEntry l : locks){ + if(l.channel.equals(channel) && l.lock.equals(lock) && l.treeNode.equals(treeNode)){ + add = false; + break; + } + } + if(add) + locks.add(new LockEntry(treeNode,lock,channel)); +// System.out.println("lock after request:"+lockStatusDescription(diagramName)+"\n----"); + return true; + } + /** + * Release a lock previously acquired + * @param treeNode the tree node, whose lock is getting released + * @param lock the lock type + * @param channel the channel of the client releasing the lock + * @param diagramName the diagram whose tree node is affected by this call + * + * @return true if a lock was really yielded as a result of the call + */ + public boolean releaseLock(DiagramTreeNode treeNode, Lock lock, SocketChannel channel, String diagramName){ + List<LockEntry> locks = locksMap.get(diagramName); + Iterator<LockEntry> iterator = locks.iterator(); + boolean lockReleased = false; + while(iterator.hasNext()){ + LockEntry entry = iterator.next(); + if(entry.treeNode.equals(treeNode) && entry.lock == lock && entry.channel == channel){ + iterator.remove(); + lockReleased = true; + if(lock == Lock.DELETE && treeNode instanceof Node) + continue; // we have to check for attached edges which must be unlocked too + else + break;// if ain't a delete lock, we found what looking for and we can stop + } + /* if a delete lock, we have to check for attached edges which must be unlocked too */ + if(lock == Lock.DELETE && entry.treeNode instanceof Edge && treeNode instanceof Node){ + Edge e = (Edge)entry.treeNode; + if(e.getNodesNum() == 2){ + if(e.getNodeAt(0).equals(treeNode) || e.getNodeAt(1).equals(treeNode)){ + iterator.remove(); + } + } + } + } +// System.out.println("lock release:"+lockStatusDescription(diagramName)+"\n----"); + return lockReleased; + } + + /** + * Removes all the locks related to a diagram element, to be called when a diagram element is deleted. + * If the diagram element is a Node all the locks of the two-ended edges attached to it will be + * removed as well. + * @param element the diagram element whose locks must be removed + * @param diagramName the diagram the element has to be removed from + */ + public void removeLocks(DiagramElement element, String diagramName){ + List<LockEntry> locks = locksMap.get(diagramName); + if(locks == null) + return; + Iterator<LockEntry> iterator = locks.iterator(); + boolean isNode = (element instanceof Node); + while(iterator.hasNext()){ + LockEntry entry = iterator.next(); + if(entry.treeNode.equals(element)){ + iterator.remove(); + } + /* remove the lock id it's a two ended edges locks attached to this Node */ + if(isNode && entry.treeNode instanceof Edge){ + Edge e = (Edge)entry.treeNode; + if(e.getNodesNum() > 2) + continue; + if(e.getNodeAt(0).equals(element) || e.getNodeAt(1).equals(element)){ + iterator.remove(); + } + } + } + } + + /** + * remove all the locks acquired by a client, this is normally called when a client disconnects + * @param channel the channel uniquely identifying the client + * @param diagramName the name of the diagram the channel is related to + */ + public void removeLocks(SocketChannel channel, String diagramName){ + if(!locksMap.containsKey(diagramName)) + /* this can happen if a client connects and downloads the diagram list * + * and then disconnects without opening any diagram */ + return; + Iterator<LockEntry> itr = locksMap.get(diagramName).iterator(); + while(itr.hasNext()) + if(itr.next().channel.equals(channel)) + itr.remove(); + } + + public String lockStatusDescription(String diagramName){ + StringBuilder builder = new StringBuilder(); + if(locksMap.containsKey(diagramName)){ + List<LockEntry> locks = locksMap.get(diagramName); + builder.append(diagramName).append('\n'); + for(LockEntry entry : locks){ + builder.append(entry.channel.socket().getInetAddress().getHostAddress()).append(' '). + append(entry.lock).append(' '). + append(entry.treeNode.getName()).append('\n'); + } + } + return builder.toString(); + } + + public void clearAllLocks(){ // should not be used unless in a debugging session + locksMap = new HashMap<String,List<LockEntry>>(); + } + + private Map<String,List<LockEntry>> locksMap; + + private static class LockEntry { + public LockEntry(DiagramTreeNode treeNode,Lock lock,SocketChannel channel) { + this.channel = channel; + this.lock = lock; + this.treeNode = treeNode; + } + + public SocketChannel channel; + public Lock lock; + public DiagramTreeNode treeNode; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/network/ServerNotRunningException.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,30 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.network; + +/** + * Exception thrown when the user tries to share a diagram before starting the server + */ +@SuppressWarnings("serial") +public class ServerNotRunningException extends DiagramShareException { + ServerNotRunningException(String msg){ + super(msg); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/pdsupport/PdConnection.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,132 @@ +/* + accessPD - An accessible PD patches editor + + Copyright (C) 2014 Fiore Martin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.pdsupport; + +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; +import java.io.InputStream; + +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.GraphElement; +import uk.ac.qmul.eecs.ccmi.gui.LineStyle; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +public class PdConnection extends Edge implements PdElement{ + private static final long serialVersionUID = 1L; + private static InputStream sound; + private static String FROM = "from"; + private static String TO = "to"; + + static{ + sound = PdConnection.class.getResourceAsStream("audio/PdConnection.mp3"); + SoundFactory.getInstance().loadSound(sound); + } + + public PdConnection(){ + super("Connection", new String[] {FROM, TO}, 2, 2, LineStyle.Solid); + } + + + @Override + public void draw(Graphics2D g2) { + /* use this edge stroke */ + Stroke oldStroke = g2.getStroke(); + g2.setStroke(getStyle().getStroke()); + /* straight line */ + + if(points.isEmpty()){ + /* just one line from one node to the other */ + Line2D line = getSegment(getNodeAt(0),getNodeAt(1)); + g2.draw(line); + }else{ + /* the edge has been bended into more lines. * + * for every inner point neighbour draw a line * + * and draw the inner point itself */ + for(InnerPoint p : points){ + for(GraphElement ge : p.getNeighbours()){ + g2.draw(getSegment(p,ge)); + } + p.draw(g2); + } + + } + + /* restore old stroke of g2 */ + g2.setStroke(oldStroke); + } + + @Override + public Rectangle2D getBounds() { + Rectangle2D bounds = (Rectangle2D)getNodeAt(0).getBounds(); + + for(int i=1; i< getNodesNum(); i++) + bounds.add(getNodeAt(i).getBounds()); + + for(InnerPoint p : points){ + bounds.add(p.getBounds()); + } + + return bounds; + } + + @Override + public InputStream getSound() { + return sound; + } + + + @Override + public String toPdFile() { + + Node n0 = getNodeAt(0); + Node n1 = getNodeAt(1); + + PdElement elemFrom = (PdElement) ( FROM.equals(this.getEndDescription(n0)) ? n0 : n1); + PdElement elemTo = (PdElement) ( TO.equals(this.getEndDescription(n0)) ? n0 : n1); + + return getChunckType() + + " connect " + + elemFrom.getOrderNumber()+ + " 0 " + + elemTo.getOrderNumber() + + " 0" + ; + } + + + @Override + public String getChunckType() { + return "#X"; + } + + @Override + public int getOrderNumber(){ + throw new UnsupportedOperationException(); + } + + public Object clone(){ + + return new PdConnection(); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/pdsupport/PdDiagram.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,32 @@ +/* + accessPD - An accessible PD patches editor + + Copyright (C) 2014 Fiore Martin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.pdsupport; + +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Node; + +public class PdDiagram extends Diagram.LocalDiagram { + private static Node[] nodes = {new PdObject(), new PdNumber()}; + private static Edge[] edges = {new PdConnection()}; + + public PdDiagram(){ + super("PD Patch.pd",nodes,edges,null); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/pdsupport/PdElement.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,29 @@ +/* + accessPD - An accessible PD patches editor + + Copyright (C) 2014 Fiore Martin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.pdsupport; + +public interface PdElement { + public String toPdFile(); + + public String getChunckType(); + + public int getOrderNumber(); + +} + \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/pdsupport/PdNumber.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,66 @@ +/* + accessPD - An accessible PD patches editor + + Copyright (C) 2014 Fiore Martin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.pdsupport; + +import java.awt.geom.Rectangle2D; +import java.io.InputStream; + +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +public class PdNumber extends PdObject implements PdElement { + private static final long serialVersionUID = 1L; + private static InputStream asound; + + static{ + asound = PdNumber.class.getResourceAsStream("audio/PdNumber.mp3"); + SoundFactory.getInstance().loadSound(asound); + } + + public PdNumber(){ + super("Number"); + } + + @Override + public String toPdFile(){ + Rectangle2D bounds = getBounds(); + return + getChunckType() + + " floatatom " + + ((int)bounds.getX()) + + ' ' + + ((int)bounds.getY()) + + " 5 0 0 0 "+getName()+" - -"; + + } + + @Override + protected Rectangle2D.Double getMinBounds(){ + return new Rectangle2D.Double(0,0,150,40); + } + + @Override + public String getChunckType() { + return "#X"; + } + + @Override + public InputStream getSound() { + return asound; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/pdsupport/PdObject.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,207 @@ +/* + accessPD - An accessible PD patches editor + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + Copyright (C) 2014 Fiore Martin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.pdsupport; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.InputStream; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.simpletemplate.MultiLineString; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +public class PdObject extends Node implements PdElement { + private static final long serialVersionUID = 1L; + private static final int DEFAULT_WIDTH = 100; + private static final int DEFAULT_HEIGHT = 60; + private static final Rectangle2D.Double minBounds = new Rectangle2D.Double(0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT); + + + private Rectangle2D.Double bounds; + private static InputStream sound; + private MultiLineString label; + private int orderNumber; + + static{ + sound = PdObject.class.getResourceAsStream("audio/PdObject.mp3"); + SoundFactory.getInstance().loadSound(sound); + } + + public PdObject(String type) { + super(type, NodeProperties.NULL_PROPERTIES); + bounds = new Rectangle2D.Double(0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT); + label = new MultiLineString(); + label.setText(getType()); + } + + public PdObject() { + this("Object"); + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy) { + bounds.setFrame(bounds.getX() + dx, + bounds.getY() + dy, + bounds.getWidth(), + bounds.getHeight()); + + } + + @Override + public boolean contains(Point2D aPoint) { + return bounds.contains(aPoint); + } + + @Override + public Rectangle2D getBounds() { + return bounds.getBounds2D(); + } + + @Override + public Point2D getConnectionPoint(Direction d) { + double slope = bounds.getHeight() / bounds.getWidth(); + double ex = d.getX(); + double ey = d.getY(); + double x = bounds.getCenterX(); + double y = bounds.getCenterY(); + + if (ex != 0 && -slope <= ey / ex && ey / ex <= slope){ + // intersects at left or right boundary + if (ex > 0){ + x = bounds.getMaxX(); + y += (bounds.getWidth() / 2) * ey / ex; + }else{ + x = bounds.getX(); + y -= (bounds.getWidth() / 2) * ey / ex; + } + }else if (ey != 0){ + // intersects at top or bottom + if (ey > 0){ + x += (bounds.getHeight() / 2) * ex / ey; + y = bounds.getMaxY(); + }else{ + x -= (bounds.getHeight() / 2) * ex / ey; + y = bounds.getY(); + } + } + + return new Point2D.Double(x, y); + + } + + @Override + public Shape getShape() { + return getBounds(); + } + + @Override + public InputStream getSound() { + return sound; + } + + @Override + public void draw(Graphics2D g2d){ + Color oldColor = g2d.getColor(); + g2d.setColor(Color.WHITE); + g2d.fill(getBounds()); + + g2d.setColor(oldColor); + + label.draw(g2d, getBounds()); + g2d.draw(bounds); + } + + @Override + public void setName(String name, Object source){ + label.setText(name); + super.setName(name, source); + } + + @Override + public void setId(long id){ + super.setId(id); + + label.setText(getName()); + /* when they are given an id nodes change name into "new <type> node <id>" * + * where <type> is the actual type of the node and <id> is the given id * + * therefore a reshape is necessary to display the new name */ + Rectangle2D boundsBeforeReshape = getBounds(); + + bounds = calculateBounds(label.getBounds()); + + /* the reshape might change the bounds, so the shape is translated so that the top-left * + * point is at the same position as before just to keep it more consistent */ + Rectangle2D boundsAfterReshape = getBounds(); + translateImplementation( + new Point2D.Double(), + boundsBeforeReshape.getX() - boundsAfterReshape.getX(), + boundsBeforeReshape.getY() - boundsAfterReshape.getY() + ); + + + } + + protected Rectangle2D.Double getMinBounds(){ + return minBounds; + } + + private Rectangle2D.Double calculateBounds(Rectangle2D bounds){ + return new Rectangle2D.Double(bounds.getX() , bounds.getY(), + Math.max(bounds.getWidth(), getMinBounds().getWidth()), + Math.max(bounds.getHeight(), getMinBounds().getHeight()) + ); + } + + @Override + public Object clone(){ + PdObject clone = (PdObject)super.clone(); + + clone.bounds = new Rectangle2D.Double(0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT); + clone.label = new MultiLineString(); + clone.label.setText(getType()); + + return clone; + } + + public String toPdFile(){ + Rectangle2D bounds = getBounds(); + return getChunckType() + " obj " + ((int)bounds.getX()) + + ' ' + ((int)bounds.getY()) + ' ' + getName().replaceAll("\\s+",""); + } + + @Override + public String getChunckType() { + return "#X"; + } + + public void setOrderNumber(int n){ + orderNumber = n; + } + + @Override + public int getOrderNumber(){ + return orderNumber; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/pdsupport/PdPersistenceManager.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,77 @@ +package uk.ac.qmul.eecs.ccmi.pdsupport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; + +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.Node; +/* +accessPD - An accessible PD patches editor + +Copyright (C) 2014 Fiore Martin + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +public class PdPersistenceManager { + private static PdPersistenceManager singleton; + public final static String PD_EXTENSION = ".pd"; + + public static PdPersistenceManager getInstance(){ + if(singleton == null){ + singleton = new PdPersistenceManager(); + } + + return singleton; + } + + public void encodeDiagramInstance(Diagram diagram, String newName, OutputStream out){ + OutputStreamWriter writer = new OutputStreamWriter(out, Charset.forName("US-ASCII")); + + PrintWriter printWriter = new PrintWriter(writer); + + printWriter.print("#N canvas 0 0 550 400 10;\r\n"); + + + int index = 0; + for(Node n : diagram.getCollectionModel().getNodes()){ + PdObject pdObj = (PdObject)n; + + pdObj.setOrderNumber(index++); + printWriter.print(pdObj.toPdFile()+";\r\n"); + } + + for(Edge e : diagram.getCollectionModel().getEdges()){ + PdConnection pdCon = (PdConnection)e; + printWriter.print(pdCon.toPdFile()+";\r\n"); + } + + printWriter.close(); + } + + public Diagram decodeDiagramInstance(InputStream in) throws IOException { + return null; + } + + public void encodeDiagramInstance(Diagram diagram, OutputStream out) throws IOException{ + encodeDiagramInstance(diagram, diagram.getName(),out); + } + + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/ArrowHead.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,131 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.io.IOException; + +/** + This class defines arrowheads of diverse shapes. +*/ +public enum ArrowHead { + TAIL("Tail"), + TRIANGLE("Triangle"), + BLACK_TRIANGLE("Black Triangle"), + V("V"), + HALF_V("Half V"), + DIAMOND("Diamond"), + BLACK_DIAMOND("Black Diamond"); + + private ArrowHead(String name) { + this.lowerCaseName = name; + } + + public static ArrowHead getArrowHeadFromString(String arrowHeadName) throws IOException{ + ArrowHead h; + String name = arrowHeadName.toUpperCase().replace(" ", "_"); + try { + h = ArrowHead.valueOf(name); + }catch (IllegalArgumentException e){ + throw new IOException(e); + } + return h; + } + + @Override + public String toString(){ + return lowerCaseName; + } + + /** + Draws the arrowhead. + @param g2 the graphics context + @param p a point on the axis of the arrow head + @param q the end point of the arrow head + */ + public void draw(Graphics2D g2, Point2D p, Point2D q){ + GeneralPath path = getPath(p, q); + Color oldColor = g2.getColor(); + if (this == BLACK_DIAMOND || this == BLACK_TRIANGLE) + g2.setColor(Color.BLACK); + else + g2.setColor(Color.WHITE); + g2.fill(path); + g2.setColor(oldColor); + Stroke oldStroke = g2.getStroke(); + g2.setStroke(new BasicStroke()); + g2.draw(path); + g2.setStroke(oldStroke); + } + + /** + Gets the path of the arrowhead + @param p a point on the axis of the arrow head + @param q the end point of the arrow head + @return the path + */ + public GeneralPath getPath(Point2D p, Point2D q){ + GeneralPath path = new GeneralPath(); + final double ARROW_ANGLE = Math.PI / 6; + final double ARROW_LENGTH = 10; + + double dx = q.getX() - p.getX(); + double dy = q.getY() - p.getY(); + double angle = Math.atan2(dy, dx); + double x1 = q.getX() + - ARROW_LENGTH * Math.cos(angle + ARROW_ANGLE); + double y1 = q.getY() + - ARROW_LENGTH * Math.sin(angle + ARROW_ANGLE); + double x2 = q.getX() + - ARROW_LENGTH * Math.cos(angle - ARROW_ANGLE); + double y2 = q.getY() + - ARROW_LENGTH * Math.sin(angle - ARROW_ANGLE); + + path.moveTo((float)q.getX(), (float)q.getY()); + path.lineTo((float)x1, (float)y1); + if (this == V) + { + path.moveTo((float)x2, (float)y2); + path.lineTo((float)q.getX(), (float)q.getY()); + } + else if (this == TRIANGLE || this == BLACK_TRIANGLE) + { + path.lineTo((float)x2, (float)y2); + path.closePath(); + } + else if (this == DIAMOND || this == BLACK_DIAMOND) + { + double x3 = x2 - ARROW_LENGTH * Math.cos(angle + ARROW_ANGLE); + double y3 = y2 - ARROW_LENGTH * Math.sin(angle + ARROW_ANGLE); + path.lineTo((float)x3, (float)y3); + path.lineTo((float)x2, (float)y2); + path.closePath(); + } + return path; + } + + private String lowerCaseName; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/CircleNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,138 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + * A cricle shaped diagram node. + * + */ +@SuppressWarnings("serial") +public class CircleNode extends EllipticalNode { + + public CircleNode(String typeName, NodeProperties properties) { + super(typeName, properties); + dataDisplayBounds = (Rectangle2D.Double)getMinBounds(); + cShape = new Ellipse2D.Double(); + cShape.setFrame(dataDisplayBounds); + } + + @Override + public Rectangle2D getMinBounds(){ + Rectangle2D r = super.getMinBounds(); + r.setFrame(r.getX(), r.getY(), r.getHeight(), r.getHeight()); + return (Rectangle2D)r; + } + + @Override + public Rectangle2D getBounds(){ + return cShape.getBounds2D(); + } + + @Override + public Shape getShape(){ + return cShape; + } + + @Override + public InputStream getSound(){ + return sound; + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + cShape.setFrame(cShape.getX() + dx, + cShape.getY() + dy, + cShape.getWidth(), + cShape.getHeight()); + super.translateImplementation(p,dx, dy); + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + super.reshapeInnerProperties(insidePropertyTypes); + double diffwh = dataDisplayBounds.getWidth() - dataDisplayBounds.getHeight(); + Rectangle2D.Double r = new Rectangle2D.Double(); + if(diffwh > 0){ + r.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY()-diffwh/2,dataDisplayBounds.getWidth(),dataDisplayBounds.getWidth()); + } else if(diffwh < 0){ + r.setFrame(dataDisplayBounds.getX()+diffwh/2,dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + }else{ + r.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + } + cShape.setFrame(super.anyInsideProperties() ? getOutBounds(r) : r); + } + + @Override + public void decode(Document doc, Element nodeTag) throws IOException{ + super.decode(doc, nodeTag); + double diffwh = dataDisplayBounds.getWidth() - dataDisplayBounds.getHeight(); + Rectangle2D.Double r = new Rectangle2D.Double(); + if(diffwh > 0){ + r.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY()-diffwh/2,dataDisplayBounds.getWidth(),dataDisplayBounds.getWidth()); + } else if(diffwh < 0){ + r.setFrame(dataDisplayBounds.getX()+diffwh/2,dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + }else{ + r.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + } + cShape.setFrame(super.anyInsideProperties() ? getOutBounds(r) : r); + + } + + @Override + public ShapeType getShapeType(){ + return ShapeType.Circle; + } + + @Override + public Object clone(){ + CircleNode n = (CircleNode)super.clone(); + n.cShape = (Ellipse2D.Double)cShape.clone(); + return n; + } + + private Ellipse2D.Double cShape; + private static InputStream sound; + static{ + sound = CircleNode.class.getResourceAsStream("audio/Circle.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/EdgeDrawSupport.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,158 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import javax.swing.JLabel; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNode; +import uk.ac.qmul.eecs.ccmi.gui.GraphPanel; + + +/** + * Provides static methods to draw a {@code SimpleShapeEdge} on a {@code Graphics} + * + */ +public abstract class EdgeDrawSupport { + /** + * Draws a string in proximity of a edge. + * @param g2 the graphics context + * @param p an endpoint of the segment along which to + * draw the string + * @param q the other endpoint of the segment along which to + * draw the string + * @param arrow the arrow head painted on the edge end where the string + * is to be painted + * @param s the string to draw + * @param center true if the string should be centered + * along the segment + */ + + public static void drawString(Graphics2D g2, + Point2D p, Point2D q, ArrowHead arrow, String s, boolean center){ + if (s == null || s.length() == 0) return; + label.setText("<html>" + s + "</html>"); + label.setFont(g2.getFont()); + Dimension d = label.getPreferredSize(); + label.setBounds(0, 0, d.width, d.height); + + Rectangle2D b = getStringBounds(g2, p, q, arrow, s, center); + + Color oldColor = g2.getColor(); + g2.setColor(g2.getBackground()); + g2.fill(b); + g2.setColor(oldColor); + + g2.translate(b.getX(), b.getY()); + label.paint(g2); + g2.translate(-b.getX(), -b.getY()); + } + + /** + * Draws a graphical marker when the edge has notes associated to it + * @see uk.ac.qmul.eecs.ccmi.diagrammodel.TreeModel#setNotes(DiagramTreeNode, String, Object) + * + * @param g2 the graphics context + * @param p an endpoint of the segment along which to draw the string + * @param q the other endpoint of the segment along which to draw the string + */ + public static void drawMarker(Graphics2D g2, Point2D p, Point2D q){ + Point2D attach = q; + if (p.getX() > q.getX()){ + drawMarker(g2, q, p); + return; + } + attach = new Point2D.Double((p.getX() + q.getX()) / 2, + (p.getY() + q.getY()) / 2); + Color oldColor = g2.getColor(); + g2.setColor(GraphPanel.GRABBER_COLOR); + g2.fill(new Rectangle2D.Double(attach.getX() - MARKER_SIZE / 2, attach.getY() - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE)); + g2.setColor(oldColor); + } + + /* + Computes the attachment point for drawing a string. + return the point at which to draw the string + */ + private static Point2D getAttachmentPoint(Graphics2D g2, + Point2D p, Point2D q, ArrowHead arrow, Dimension d, boolean center){ + final int GAP = 3; + double xoff = GAP; + double yoff = -GAP - d.getHeight(); + Point2D attach = q; + if (center){ + if (p.getX() > q.getX()){ + return getAttachmentPoint(g2, q, p, arrow, d, center); + } + attach = new Point2D.Double((p.getX() + q.getX()) / 2, + (p.getY() + q.getY()) / 2); + if (p.getY() < q.getY()) + yoff = - GAP - d.getHeight(); + else if (p.getY() == q.getY()) + xoff = -d.getWidth() / 2; + else + yoff = GAP; + } + else + { + if (p.getX() < q.getX()){ + xoff = -GAP - d.getWidth(); + } + if (p.getY() > q.getY()){ + yoff = GAP; + } + if (arrow != null){ + Rectangle2D arrowBounds = arrow.getPath(p, q).getBounds2D(); + if (p.getX() < q.getX()){ + xoff -= arrowBounds.getWidth(); + } + else{ + xoff += arrowBounds.getWidth(); + } + } + } + return new Point2D.Double(attach.getX() + xoff, attach.getY() + yoff); + } + + /* + * Computes the extent of a string that is drawn along a line segment. + * The rectangle enclosing the string + */ + private static Rectangle2D getStringBounds(Graphics2D g2, + Point2D p, Point2D q, ArrowHead arrow, String s, boolean center){ + if (g2 == null) return new Rectangle2D.Double(); + if (s == null || s.equals("")) return new Rectangle2D.Double(q.getX(), q.getY(), 0, 0); + label.setText("<html>" + s + "</html>"); + label.setFont(g2.getFont()); + Dimension d = label.getPreferredSize(); + Point2D a = getAttachmentPoint(g2, p, q, arrow, d, center); + return new Rectangle2D.Double(a.getX(), a.getY(), d.getWidth(), d.getHeight()); + } + + /* size of the marker when an edge as notes */ + private static final int MARKER_SIZE = 7; + private static JLabel label = new JLabel(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/EllipticalNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,153 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + * An elliptical shaped diagram node. + * + */ +@SuppressWarnings("serial") +public class EllipticalNode extends SimpleShapeNode { + + public EllipticalNode(String typeName, NodeProperties properties){ + super(typeName, properties); + Rectangle2D r = getMinBounds(); + eShape = new Ellipse2D.Double(0,0,r.getWidth(),r.getHeight()); + } + + @Override + public ShapeType getShapeType() { + return ShapeType.Ellipse; + } + + public static Rectangle2D getOutBounds(Rectangle2D r){ + double h = r.getHeight()/2; + double w = r.getWidth()/2; + + double anglew = Math.atan(h/w); + double angleh = Math.atan(w/h); + + double a = w + h * Math.tan(angleh)/2; + double b = h + w * Math.tan(anglew)/2; + + return new Rectangle2D.Double( + r.getCenterX() - a, + r.getCenterY() - b, + 2*a, + 2*b + ); + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + super.reshapeInnerProperties(insidePropertyTypes); + eShape.setFrame(super.anyInsideProperties() ? getOutBounds(dataDisplayBounds) : dataDisplayBounds ); + } + + @Override + public void decode(Document doc, Element nodeTag) throws IOException{ + super.decode(doc, nodeTag); + eShape.setFrame(super.anyInsideProperties() ? getOutBounds(dataDisplayBounds) : dataDisplayBounds ); + } + + @Override + protected void translateImplementation(Point2D p,double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + eShape.setFrame(eShape.getX() + dx, + eShape.getY() + dy, + eShape.getWidth(), + eShape.getHeight()); + super.translateImplementation(p,dx, dy); + } + + @Override + public Rectangle2D getBounds() { + return eShape.getBounds2D(); + } + + @Override + public Point2D getConnectionPoint(Direction d) { + return calculateConnectionPoint(d, getBounds()); + } + + public static Point2D calculateConnectionPoint(Direction d, Rectangle2D bounds){ + double a = bounds.getWidth() / 2; + double b = bounds.getHeight() / 2; + double x = d.getX(); + double y = d.getY(); + double cx = bounds.getCenterX(); + double cy = bounds.getCenterY(); + + if (a != 0 && b != 0 && !(x == 0 && y == 0)){ + double t = Math.sqrt((x * x) / (a * a) + (y * y) / (b * b)); + return new Point2D.Double(cx + x / t, cy + y / t); + } + else{ + return new Point2D.Double(cx, cy); + } + } + + @Override + public Shape getShape() { + return eShape; + } + + @Override + public InputStream getSound(){ + return sound; + } + + @Override + public Object clone(){ + EllipticalNode n = (EllipticalNode)super.clone(); + n.eShape = (Ellipse2D.Double)eShape.clone(); + return n; + } + + private Ellipse2D.Double eShape; + private static InputStream sound; + static{ + sound = EllipticalNode.class.getResourceAsStream("audio/Ellipse.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/Model.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,405 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.ResourceBundle; + +/* + * The Wizard Model holds all the data the user enters during the process of creating a diagram template. + * Such data can be re-edited by the user during the same process or if they want to create + * a new diagram template out of an existing one. When the user has entered all the data the model is used + * to create an instance of the {@link Diagram class} which is then used as a prototype diagram. + * Diagram instances that the user will edit are created by cloning the relating prototype diagram. + * + */ +class Model { + Model(){ + diagramName = new Record(); + nodes = new ModelMap<Node>(); + edges = new ModelMap<Edge>(); + } + + @Override + public String toString(){// FIXME need to port the strings to the .properties file + StringBuilder builder = new StringBuilder(); + builder.append("Diagram Name is ").append(diagramName.value).append(".\n\n"); + builder.append(diagramName.value + " diagram has "+ (nodes.isEmpty() ? "no" : nodes.size()) + " node"+((nodes.size() == 1) ? "" : "s")); + if(!nodes.isEmpty()){ + builder.append(": "); + int i = 0; + for(Node n : nodes.values()){ + builder.append(n.type.value); + if(nodes.values().size() == 1) + break; + if(i == nodes.values().size() - 2){ + builder.append(" and "); + }else if(i < nodes.values().size()-1){ + builder.append(", "); + } + i++; + } + } + builder.append(".\n"); + for(Node n : nodes.values()){ + builder.append(n.type+" node has a ") + .append(n.shape + " shape.\n"); + builder.append(n.type+" node has "+ (n.properties.isEmpty() ? "no" : n.properties.size())+((n.properties.size() == 1) ? " property" : " properties")); + if(!n.properties.isEmpty()){ + builder.append(": "); + int i = 0; + for(Property p : n.properties.values()){ + builder.append(p.type.value); + if(n.properties.size() == 1) + break; + if(i == n.properties.size() - 2){ + builder.append(" and "); + }else if(i < n.properties.size()-1){ + builder.append(", "); + } + i++; + } + } + builder.append(".\n"); + for(Property p : n.properties.values()){ + builder.append(p.type+" property has position "+p.position); + if(p.position.value.equals(SimpleShapeNode.Position.Outside.toString())) + builder.append(" and shape "+p.shape+".\n"); + else + builder.append(".\n"); + builder.append(p.type+" property has "+(p.modifiers.isEmpty() ? "no" : p.modifiers.size())+(p.modifiers.size() == 1 ? " modifier" : " modifiers")); + if(!p.modifiers.isEmpty()){ + builder.append(": "); + int i = 0; + for(Modifier m : p.modifiers.values()){ + builder.append(m.type.value); + if(p.modifiers.size() == 1) + break; + if(i == p.modifiers.size() - 2){ + builder.append(" and "); + }else if(i < p.modifiers.size()-1){ + builder.append(", "); + } + i++; + } + } + builder.append(".\n"); + for(Modifier m : p.modifiers.values()){ + builder.append(m.type+ " modifier "); + if(m.format.values.length > 0) + builder.append("is formatted as "+m.format+".\n"); + else + builder.append("\n"); + } + } + } + builder.append('\n'); + builder.append(diagramName.value + " diagram has "+ (edges.isEmpty() ? "no" : edges.size()) + " edge"+((edges.size() == 1) ? "" : "s")); + if(!edges.isEmpty()){ + builder.append(": "); + int i = 0; + for(Edge e : edges.values()){ + builder.append(e.type.value); + if(edges.values().size() == 1) + break; + if(i == edges.values().size() - 2){ + builder.append(" and "); + }else if(i < edges.values().size()-1){ + builder.append(", "); + } + i++; + } + } + builder.append(".\n"); + for(Edge e : edges.values()){ + builder.append(e.type+ " edge has minimum nodes "+e.minNodes+" and maximum nodes "+e.maxNodes+".\n") + .append(e.type+ " edge has a "+ e.lineStyle+" line style.\n") + .append(e.type+" edge has "+ ((e.arrowHeads.values.length == 0) ? "no harrow heads.\n" : e.arrowHeads.values.length +" harrow heads: "+e.arrowHeads+".\n")); + } + builder.append('\n'); + builder.append("Press up and down arrow keys to go through the summary.\n"); + return builder.toString(); + } + + Record diagramName; + /* these are sets as when we edit an already existing node we just add */ + /* to the set the new node and it gets replaced */ + ModelMap<Node> nodes; + ModelMap<Edge> edges; + + static Element copy(Element src, Element dest){ + if(src instanceof Node){ + copy((Node)src,(Node)dest); + }else if (src instanceof Edge){ + copy((Edge)src,(Edge)dest); + }else if(src instanceof Property){ + copy((Property)src,(Property)dest); + }else{ + copy((Modifier)src,(Modifier)dest); + } + return dest; + } + + static void copy(Node src, Node dest){ + dest.id = src.id; + dest.type.value = src.type.value; + dest.shape.value = src.shape.value; + dest.properties.clear(); + dest.properties.putAll(src.properties); + } + + static void copy(Property src, Property dest){ + dest.id = src.id; + dest.type.value = src.type.value; + dest.shape.value = src.shape.value; + dest.position.value = src.position.value; + dest.modifiers.clear(); + dest.modifiers.putAll(src.modifiers); + } + + static void copy(Edge src, Edge dest){ + dest.id = src.id; + dest.type.value = src.type.value; + dest.lineStyle.value = src.lineStyle.value; + dest.maxNodes.value = src.maxNodes.value; + dest.minNodes.value = src.minNodes.value; + dest.arrowHeads.values = src.arrowHeads.values; + dest.arrowHeadsDescriptions.values = src.arrowHeadsDescriptions.values; + } + + static void copy(Modifier src, Modifier dest){ + dest.id = src.id; + dest.type.value = src.type.value; + dest.format.values = src.format.values; + dest.affix.values = src.affix.values; + } + + static class Record{ + Record(){ + value = ""; + } + Record(String value){ + this.value = value; + } + @Override + public String toString(){ + return value; + } + String value; + } + + static class StrArrayRecord{ + String [] values; + StrArrayRecord(){ + values = new String[0]; + } + + StrArrayRecord(String[] values){ + this.values = values; + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(int i=0; i<values.length; i++){ + builder.append(values[i]); + if(values.length == 1) + break; + if(i == values.length - 2) + builder.append(" and "); + else if(i < values.length -1 ) + builder.append(", "); + } + return builder.toString(); + } + } + + private static long uniqueId = 0; + static class Element { + Element(){ + type = new Record(); + id = uniqueId++; + } + + void clear () { + type.value = ""; + id = uniqueId++; + } + long id; + Record type; + } + + static class Node extends Element{ + Node(){ + shape = new Record(); + properties = new ModelMap<Property>(); + } + + @Override + void clear(){ + super.clear(); + shape.value = ""; + properties.clear(); + } + Record shape; + ModelMap<Property> properties; + } + + static class Edge extends Element { + Edge(){ + lineStyle = new Record(); + minNodes = new Record(); + maxNodes = new Record(); + arrowHeads = new StrArrayRecord(){ + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + for(int i=0; i<values.length; i++){ + builder.append(values[i]+" with label "+arrowHeadsDescriptions.values[i]); + if(values.length == 1) + break; + if(i == values.length - 2) + builder.append(" and "); + else if(i < values.length -1 ) + builder.append(", "); + } + return builder.toString(); + } + }; + arrowHeadsDescriptions = new StrArrayRecord(); + } + @Override + public void clear(){ + super.clear(); + lineStyle.value = ""; + minNodes.value = ""; + maxNodes.value = ""; + arrowHeads.values = new String[0]; + arrowHeadsDescriptions.values = new String[0]; + } + Record lineStyle; + Record minNodes; + Record maxNodes; + StrArrayRecord arrowHeads; + StrArrayRecord arrowHeadsDescriptions; + } + + static class Property extends Element{ + Property(){ + position = new Model.Record(); + shape = new Model.Record(); + modifiers = new ModelMap<Modifier>(); + } + Record position; + Record shape; + ModelMap<Modifier> modifiers; + + @Override + void clear(){ + super.clear(); + modifiers.clear(); + position.value = ""; + shape.value = ""; + } + } + + static class Modifier extends Element { + StrArrayRecord format; + StrArrayRecord affix; + + Modifier(){ + affix = new StrArrayRecord(); + format = new StrArrayRecord(){ + @Override + public String toString(){ + ResourceBundle resources = ResourceBundle.getBundle(SpeechWizardDialog.class.getName()); + StringBuilder builder = new StringBuilder(); + for(int i=0; i<values.length; i++){ + builder.append(values[i]); + if(values[i].equals(resources.getString("modifier.format.prefix")) && !affix.values[0].isEmpty()){ + builder.append(' ').append(affix.values[0]); + } + if(values[i].equals(resources.getString("modifier.format.suffix")) && !affix.values[1].isEmpty()){ + builder.append(' ').append(affix.values[1]); + } + + if(values.length == 1) + break; + if(i == values.length - 2) + builder.append(" and "); + else if(i < values.length -1 ) + builder.append(", "); + } + return builder.toString(); + } + }; + + /* affix is always length = 2 as it only contains prefix and suffix string */ + /* eventually it contains empty strings but its length is not variable */ + affix.values = new String[2]; + } + + @Override + void clear(){ + super.clear(); + format.values = new String[0]; + affix.values = new String[2]; + } + } +} + + +@SuppressWarnings("serial") +class ModelMap<T extends Model.Element> extends LinkedHashMap<Long,T> { + public ModelMap(){ + namesMap = new LinkedHashMap<Long,String>(); + } + + @Override + public void clear(){ + namesMap.clear(); + super.clear(); + } + + public T put(Long key, T value){ + namesMap.put(key, value.type.value); + return super.put(key, value); + } + + public void putAll(Map<? extends Long,? extends T> m){ + for(Map.Entry<? extends Long,? extends T> entry : m.entrySet()){ + namesMap.put(entry.getKey(), entry.getValue().type.value); + } + super.putAll(m); + } + + public T remove(Object key){ + namesMap.remove(key); + return super.remove(key); + } + + public Collection<String> getNames(){ + return namesMap.values(); + } + + private Map<Long,String> namesMap; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/ModifierView.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,75 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +/** + * This immutable class represents the view associated with the modifier type + * associated with it in a {@code NodeProperties} instance. + * + */ +public class ModifierView { + + public ModifierView(boolean underline, boolean bold, boolean italic, + String prefix, String postfix) { + this.underline = underline; + this.bold = bold; + this.italic = italic; + this.prefix = prefix; + this.suffix = postfix; + } + + /* Getters */ + public boolean isUnderline() { + return underline; + } + + public boolean isBold() { + return bold; + } + + public boolean isItalic() { + return italic; + } + + public String getPrefix() { + return prefix; + } + + public String getSuffix() { + return suffix; + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder("ModifierView: "); + builder.append("bold ").append(bold).append("; "); + builder.append("italic ").append(italic).append("; "); + builder.append("underline ").append(underline).append("; "); + builder.append("prefix ").append(prefix).append("; "); + builder.append("suffix ").append(suffix).append("; "); + return builder.toString(); + } + + private boolean underline; + private boolean bold; + private boolean italic; + private String prefix; + private String suffix; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/MultiLineString.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,266 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import javax.swing.JLabel; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; + +/** + A string that can extend over multiple lines. +*/ +public class MultiLineString { + /** + Constructs an empty, centered, normal size multiline + string. + */ + public MultiLineString(){ + text = ""; + arrayText = null; + justification = CENTER; + size = NORMAL; + isSingleLine = true; + } + /** + Sets the value of the text property. + @param newValue the text of the multiline string + */ + public void setText(String newValue){ + setText(newValue,null); + } + + public void setText(String newValue, ModifierView[] modifierViews) { + isSingleLine = true; + text = newValue; + this.modifierViews = modifierViews; + format(); + } + + public void setText(String[] newValue, Modifiers modifiers){ + isSingleLine = false; + this.modifiers = modifiers; + arrayText = newValue; + format(); + } + + public void setBold(boolean bold){ + isBold = bold; + format(); + } + /** + Gets the value of the text property. + @return the text of the multiline string + */ + public String getText() { + if(isSingleLine) + return text; + else{ + StringBuilder builder = new StringBuilder(""); + for(int i=0; i<arrayText.length; i++){ + builder.append(arrayText[i]); + builder.append('\n'); + } + return builder.toString(); + } + } + /** + Sets the value of the justification property. + @param newValue the justification, one of LEFT, CENTER, + RIGHT + */ + public void setJustification(int newValue) { justification = newValue; format(); } + /** + Gets the value of the justification property. + @return the justification, one of LEFT, CENTER, + RIGHT + */ + public int getJustification() { return justification; } + /** + Sets the value of the size property. + @param newValue the size, one of SMALL, NORMAL, LARGE + */ + public void setSize(int newValue) { size = newValue; format(); } + /** + Gets the value of the size property. + @return the size, one of SMALL, NORMAL, LARGE + */ + public int getSize() { return size; } + + @Override + public String toString(){ + if(isSingleLine) + return text.replace('\n', '|'); + else + return getText().replace('\n', '|'); + } + + private void format(){ + StringBuffer prefix = new StringBuffer(); + StringBuffer suffix = new StringBuffer(); + StringBuffer htmlText = new StringBuffer(); + prefix.append(" "); + suffix.insert(0, " "); + if (size == LARGE){ + prefix.append("<font size=\"+1\">"); + suffix.insert(0, "</font>"); + } + if (size == SMALL){ + prefix.append("<font size=\"-1\">"); + suffix.insert(0, "</font>"); + } + + htmlText.append("<html>"); + if(isSingleLine){ + if(isBold) + prefix.append("<b>"); + htmlText.append(prefix); + + String formattedText = text; + if(modifierViews != null){ + for(ModifierView view : modifierViews){ + formattedText = formatFromView(formattedText,view); + } + } + htmlText.append(formattedText); + + if(isBold) + suffix.insert(0, "</b>"); + htmlText.append(suffix); + }else{ // multi line + boolean first = true; + for(int i=0; i<arrayText.length; i++){ + if (first) + first = false; + else + htmlText.append("<br>"); + htmlText.append(prefix); + String textLine = arrayText[i]; + Set<Integer> indexes = modifiers.getIndexes(i); + for(Integer I : indexes){ + ModifierView view = (ModifierView)modifiers.getView(modifiers.getTypes().get(I)); + textLine = formatFromView(textLine, view); + } + htmlText.append(textLine); + htmlText.append(suffix); + } + } + htmlText.append("</html>"); + + // replace any < that are not followed by {u, i, b, tt, font, br} with < + List<String> dontReplace = Arrays.asList(new String[] { "u", "i", "b", "tt", "font", "br" }); + + int ltpos = 0; + while (ltpos != -1){ + ltpos = htmlText.indexOf("<", ltpos + 1); + if (ltpos != -1 && !(ltpos + 1 < htmlText.length() && htmlText.charAt(ltpos + 1) == '/')){ + int end = ltpos + 1; + while (end < htmlText.length() && Character.isLetter(htmlText.charAt(end))) end++; + if (!dontReplace.contains(htmlText.substring(ltpos + 1, end))) + htmlText.replace(ltpos, ltpos+1, "<"); + } + } + + label.setText(htmlText.toString()); + if (justification == LEFT) label.setHorizontalAlignment(JLabel.LEFT); + else if (justification == CENTER) label.setHorizontalAlignment(JLabel.CENTER); + else if (justification == RIGHT) label.setHorizontalAlignment(JLabel.RIGHT); + } + + private String formatFromView(String text, ModifierView view){ + if(view == null) + return text; + + StringBuilder prefix = new StringBuilder(""); + StringBuilder suffix = new StringBuilder(""); + + prefix.append(view.getPrefix()); + suffix.append(view.getSuffix()); + + if(view.isBold()){ + prefix.insert(0,"<b>"); + suffix.append("</b>"); + } + + if(view.isItalic()){ + prefix.insert(0,"<i>"); + suffix.append("</i>"); + } + + if(view.isUnderline()){ + prefix.insert(0,"<u>"); + suffix.append("</u>"); + } + return prefix.append(text).append(suffix).toString(); + } + + /** + Gets the bounding rectangle for this multiline string. + @return the bounding rectangle (with top left corner (0,0)) + */ + public Rectangle2D getBounds(){ + if(isSingleLine){ + if (text.length() == 0) + return new Rectangle2D.Double(); + }else { + if(arrayText.length == 0) + return new Rectangle2D.Double(); + } + Dimension dim = label.getPreferredSize(); + return new Rectangle2D.Double(0, 0, dim.getWidth(), dim.getHeight()); + } + + /** + Draws this multiline string inside a given rectangle + @param g2 the graphics context + @param r the rectangle into which to place this multiline string + */ + public void draw(Graphics2D g2, Rectangle2D r){ + label.setFont(g2.getFont()); + label.setBounds(0, 0, (int) r.getWidth(), (int) r.getHeight()); + g2.translate(r.getX(), r.getY()); + label.paint(g2); + g2.translate(-r.getX(), -r.getY()); + } + + public static final int LEFT = 0; + public static final int CENTER = 1; + public static final int RIGHT = 2; + public static final int LARGE = 3; + public static final int NORMAL = 4; + public static final int SMALL = 5; + + private String text; + private String[] arrayText; + private Modifiers modifiers; + private ModifierView[] modifierViews; + private int justification; + private int size; + private boolean isBold; + private transient JLabel label = new JLabel(); + private boolean isSingleLine; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/PropertyView.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,48 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapeNode.Position; +import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapeNode.ShapeType; + +/** + * This immutable class represents the view associated with the property type + * associated with it in a {@code NodeProperties} instance. + * + * */ +public class PropertyView { + public PropertyView(Position position, ShapeType shapeType) { + super(); + this.position = position; + this.shapeType = shapeType; + } + + /* Getters */ + public SimpleShapeNode.Position getPosition() { + return position; + } + + public SimpleShapeNode.ShapeType getShapeType() { + return shapeType; + } + + private SimpleShapeNode.Position position; + private SimpleShapeNode.ShapeType shapeType; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/RectangularNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,143 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + A rectangular shaped diagram node. +*/ +@SuppressWarnings("serial") +public class RectangularNode extends SimpleShapeNode{ + + public RectangularNode(String nodeType, NodeProperties properties){ + super(nodeType, properties); + bounds = dataDisplayBounds; + } + + @Override + public ShapeType getShapeType() { + return ShapeType.Rectangle; + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + + bounds.setFrame(bounds.getX() + dx, + bounds.getY() + dy, + bounds.getWidth(), + bounds.getHeight()); + super.translateImplementation(p, dx, dy); + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + super.reshapeInnerProperties(insidePropertyTypes); + bounds.setFrame(dataDisplayBounds); + } + + @Override + public void decode(Document doc, Element nodeTag) throws IOException{ + super.decode(doc, nodeTag); + bounds.setFrame(dataDisplayBounds); + } + + @Override + public Rectangle2D getBounds(){ + return (Rectangle2D)bounds.clone(); + } + + @Override + public Point2D getConnectionPoint(Direction d){ + return calculateConnectionPoint(d, getBounds()); + } + + public static Point2D calculateConnectionPoint(Direction d, Rectangle2D bounds){ + double slope = bounds.getHeight() / bounds.getWidth(); + double ex = d.getX(); + double ey = d.getY(); + double x = bounds.getCenterX(); + double y = bounds.getCenterY(); + + if (ex != 0 && -slope <= ey / ex && ey / ex <= slope){ + // intersects at left or right boundary + if (ex > 0){ + x = bounds.getMaxX(); + y += (bounds.getWidth() / 2) * ey / ex; + }else{ + x = bounds.getX(); + y -= (bounds.getWidth() / 2) * ey / ex; + } + }else if (ey != 0){ + // intersects at top or bottom + if (ey > 0){ + x += (bounds.getHeight() / 2) * ex / ey; + y = bounds.getMaxY(); + }else{ + x -= (bounds.getHeight() / 2) * ex / ey; + y = bounds.getY(); + } + } + return new Point2D.Double(x, y); + } + + public Shape getShape(){ + return bounds; + } + + @Override + public InputStream getSound(){ + return sound; + } + + public Object clone(){ + RectangularNode cloned = (RectangularNode)super.clone(); + cloned.bounds = (Rectangle2D.Double)bounds.clone(); + return cloned; + } + + private Rectangle2D.Double bounds; + private static InputStream sound; + + static{ + sound = RectangularNode.class.getResourceAsStream("audio/Rectangle.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeEdge.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,323 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; +import uk.ac.qmul.eecs.ccmi.gui.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; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * An edge rendered as a straight, dotted or dashed line. The edge can have an arrow head + * at each end. Possible arrow heads are : + * <ul> + * <li> Triangle + * <li> Black Triangle + * <li> V + * <li> Half V + * <li> Diamond + * <li> Black Diamond + * <li> Tail + * </ul> + * + */ +@SuppressWarnings("serial") +public class SimpleShapeEdge extends Edge { + + public SimpleShapeEdge(String type, LineStyle style, ArrowHead[] heads, String[] availableEndDescriptions, int minAttachedNodes, int maxAttachedNodes) { + super(type,availableEndDescriptions,minAttachedNodes,maxAttachedNodes,style); + this.heads = heads; + currentHeads = new HashMap<Node,ArrowHead>(); + } + + @Override + public boolean removeNode(DiagramNode n, Object source){ + currentHeads.remove(n); + return super.removeNode(n,source); + } + + + @Override + public void draw(Graphics2D g2) { + Stroke oldStroke = g2.getStroke(); + g2.setStroke(getStyle().getStroke()); + + /* straight line */ + if(points.isEmpty()){ + Line2D line = getSegment(getNodeAt(0),getNodeAt(1)); + g2.draw(line); + + /* draw arrow heads if any */ + ArrowHead h = currentHeads.get(getNodeAt(0)); + if( h != null && h != ArrowHead.TAIL){ + Line2D revLine = getSegment(getNodeAt(1),getNodeAt(0)); + h.draw(g2, revLine.getP1(), revLine.getP2()); + } + h = currentHeads.get(getNodeAt(1)); + if( h != null && h != ArrowHead.TAIL){ + h.draw(g2, line.getP1(), line.getP2()); + } + + /* draw labels if any */ + String label; + if((label = getEndLabel(getNodeAt(0))) != null){ + EdgeDrawSupport.drawString(g2, line.getP2(), line.getP1(), currentHeads.get(getNodeAt(0)), label, false); + } + if((label = getEndLabel(getNodeAt(1))) != null){ + EdgeDrawSupport.drawString(g2, line.getP1(), line.getP2(), currentHeads.get(getNodeAt(1)), label, false); + } + + /* draw name if any */ + if(!getName().isEmpty()){ + EdgeDrawSupport.drawString(g2, line.getP2(), line.getP1(), null, getName(), true); + } + if(!"".equals(getNotes())) + EdgeDrawSupport.drawMarker(g2,line.getP1(),line.getP2()); + }else{ + /* edge with inner points: it can be a multiended(eventually bended) edge or a straight bended edge */ + + /* arrow and labels are drawn in the same way in either case */ + for(InnerPoint p : points){ + for(GraphElement ge : p.getNeighbours()){ + g2.draw(getSegment(p,ge)); + if(ge instanceof Node){ // this is the inner point which is connected to a Node + /* draw arrow if any */ + ArrowHead h = currentHeads.get((Node)ge); + if(h != null && h != ArrowHead.TAIL){ + Line2D line = getSegment(p,ge); + h.draw(g2, line.getP1() , line.getP2()); + } + + /* draw label if any */ + String label = getEndLabel((Node)ge); + if(label != null){ + Line2D line = getSegment(p,ge); + EdgeDrawSupport.drawString(g2, line.getP1(), line.getP2(), currentHeads.get((Node)ge), label, false); + } + } + } + p.draw(g2); + } + /* name is drawn differently : + * for multiended edges name is drawn on the master inner point + * for two ends bended name is drawn in (about) the middle point of the edge + */ + + if(masterInnerPoint != null){/* multiended edge */ + Rectangle2D bounds = masterInnerPoint.getBounds(); + Point2D p = new Point2D.Double(bounds.getCenterX() - 1,bounds.getCenterY()); + Point2D q = new Point2D.Double(bounds.getCenterX() + 1,bounds.getCenterY()); + if(!getName().isEmpty()) + EdgeDrawSupport.drawString(g2, p, q, null, getName(), true); + if(!"".equals(getNotes())) + EdgeDrawSupport.drawMarker(g2,p,q); + }else{ + /* straight edge which has been bended */ + GraphElement ge1 = getNodeAt(0); + GraphElement ge2 = getNodeAt(1); + InnerPoint c1 = null; + InnerPoint c2 = null; + + for(InnerPoint innp : points){ + if(innp.getNeighbours().contains(ge1)){ + c1 = innp; + } + if(innp.getNeighbours().contains(ge2)){ + c2 = innp; + } + } + + /* draw name if any */ + if(!getName().isEmpty()){ + /* we only have two nodes but the edge has been bended */ + while((c1 != c2)&&(!c2.getNeighbours().contains(c1))){ + if(c1.getNeighbours().get(0) == ge1){ + ge1 = c1; + c1 = (InnerPoint)c1.getNeighbours().get(1); + } + else{ + ge1 = c1; + c1 = (InnerPoint)c1.getNeighbours().get(0); + } + if(c2.getNeighbours().get(0) == ge2){ + ge2 = c2; + c2 = (InnerPoint)c2.getNeighbours().get(1); + } + else{ + ge2 = c2; + c2 = (InnerPoint)c2.getNeighbours().get(0); + } + } + + Point2D p = new Point2D.Double(); + Point2D q = new Point2D.Double(); + if(c1 == c2){ + Rectangle2D bounds = c1.getBounds(); + p.setLocation( bounds.getCenterX() - 1,bounds.getCenterY()); + q.setLocation( bounds.getCenterX() + 1,bounds.getCenterY()); + }else{ + Rectangle2D bounds = c1.getBounds(); + p.setLocation( bounds.getCenterX(),bounds.getCenterY()); + bounds = c2.getBounds(); + q.setLocation(bounds.getCenterX(),bounds.getCenterY()); + + } + if(!getName().isEmpty()) + EdgeDrawSupport.drawString(g2, p, q, null, getName(), true); + if(!"".equals(getNotes())) + EdgeDrawSupport.drawMarker(g2,p,q); + } + } + } + g2.setStroke(oldStroke); + } + + public Rectangle2D getBounds() { + if(points.isEmpty()){ + return getSegment(getNodeAt(0), getNodeAt(1)).getBounds2D(); + }else{ + Rectangle2D bounds = points.get(0).getBounds(); + for(InnerPoint p : points){ + for(GraphElement ge : p.getNeighbours()) + bounds.add(getSegment(p,ge).getBounds2D()); + } + return bounds; + } + } + + public ArrowHead[] getHeads() { + return heads; + } + + @Override + public InputStream getSound(){ + switch(getStyle()){ + case Dashed : return dashedSound; + case Dotted : return dottedSound; + default : return straightSound; + } + } + + @Override + public void setEndDescription(DiagramNode diagramNode, int index, Object source){ + Node n = (Node)diagramNode; + if(index == NO_END_DESCRIPTION_INDEX){ + currentHeads.remove(n); + super.setEndDescription(n, index,source); + }else{ + ArrowHead h = heads[index]; + currentHeads.put(n, h); + super.setEndDescription(n, index,source); + } + } + + @Override + public void encode(Document doc, Element parent, List<Node> nodes){ + super.encode(doc, parent, nodes); + /* add the head attribute to the NODE tag */ + NodeList nodeTagList = parent.getElementsByTagName(PersistenceManager.NODE); + for(int i = 0 ; i< nodeTagList.getLength(); i++){ + Element nodeTag = (Element)nodeTagList.item(i); + String nodeIdAsString = nodeTag.getAttribute(PersistenceManager.ID); + long nodeId = Long.parseLong(nodeIdAsString); + Node node = null; + /* find the node with the id of the tag */ + for(Node n : nodes) + if(n.getId() == nodeId){ + node = n; + break; + } + String head = (currentHeads.get(node) == null) ? "" : currentHeads.get(node).toString(); + nodeTag.setAttribute(SimpleShapePrototypePersistenceDelegate.HEAD, head ); + } + } + + @Override + public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{ + super.decode(doc, edgeTag, nodesId); + NodeList nodeList = edgeTag.getElementsByTagName(PersistenceManager.NODE); + for(int i=0; i<nodeList.getLength(); i++){ + Element nodeTag = (Element)nodeList.item(i); + String id = nodeTag.getAttribute(PersistenceManager.ID); + if(id.isEmpty()) + throw new IOException(); + String head = nodeTag.getAttribute(SimpleShapePrototypePersistenceDelegate.HEAD); + if(!head.isEmpty()){ + ArrowHead headShape = null; + try{ + headShape = ArrowHead.getArrowHeadFromString(head); + }catch(IOException e){ + throw e; + } + currentHeads.put(nodesId.get(id), headShape); + int headDescriptionIndex = Edge.NO_END_DESCRIPTION_INDEX; + for(int j=0; j<heads.length;j++){ + if(heads[j].equals(headShape)){ + headDescriptionIndex = j; + break; + } + } + setEndDescription(nodesId.get(id),headDescriptionIndex,DiagramEventSource.PERS); + } + } + } + + @Override + public Object clone(){ + return new SimpleShapeEdge(getType(), getStyle(), heads, getAvailableEndDescriptions(), getMinAttachedNodes(), getMaxAttachedNodes() ); + } + + + + private ArrowHead[] heads; + private Map<Node,ArrowHead> currentHeads; + private static InputStream straightSound; + private static InputStream dottedSound; + private static InputStream dashedSound; + + static{ + Class<SimpleShapeEdge> c = SimpleShapeEdge.class; + straightSound = c.getResourceAsStream("audio/straightLine.mp3"); + dottedSound = c.getResourceAsStream("audio/dashedLine.mp3"); + dashedSound = c.getResourceAsStream("audio/dottedLine.mp3"); + SoundFactory.getInstance().loadSound(straightSound); + SoundFactory.getInstance().loadSound(dottedSound); + SoundFactory.getInstance().loadSound(dashedSound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,600 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RectangularShape; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; +import uk.ac.qmul.eecs.ccmi.utils.Pair; + +/** + * + * A diagram node that can be represented visually as a simple shape such as + * a rectangle, square, circle, ellipse or triangle. + * + */ +@SuppressWarnings("serial") +public abstract class SimpleShapeNode extends Node { + + public static SimpleShapeNode getInstance(ShapeType shapeType, String typeName, NodeProperties properties){ + switch(shapeType){ + case Rectangle : + return new RectangularNode(typeName, properties); + case Square : + return new SquareNode(typeName, properties); + case Circle : + return new CircleNode(typeName, properties); + case Ellipse : + return new EllipticalNode(typeName, properties); + case Triangle : + return new TriangularNode(typeName, properties); + } + return null; + } + + protected SimpleShapeNode(String typeName, NodeProperties properties){ + super(typeName, properties); + dataDisplayBounds = (Rectangle2D.Double)getMinBounds(); + /* Initialise the data structures for displaying the properties inside and outside */ + propertyNodesMap = new LinkedHashMap<String,List<PropertyNode>>(); + int numInsideProperties = 0; + for(String type : getProperties().getTypes()){ + if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside) + numInsideProperties++; + else + propertyNodesMap.put(type, new LinkedList<PropertyNode>()); + } + propertyLabels = new MultiLineString[numInsideProperties]; + nameLabel = new MultiLineString(); + } + + @Override + protected void notifyChange(ElementChangedEvent evt){ + if(!evt.getChangeType().equals("translate")&&!evt.getChangeType().equals("stop_move")){ //don't reshape for just moving + Rectangle2D boundsBeforeReshape = getBounds(); + reshape(); + Rectangle2D boundsAfterReshape = getBounds(); + /* after renaming or setting properties the boundaries can change resulting in a slight shift of the * + * node centre from its original position. the next line is to place it back to the right position */ + Point2D start = new Point2D.Double(boundsAfterReshape.getCenterX(),boundsAfterReshape.getCenterY()); + translateImplementation(start, + boundsBeforeReshape.getCenterX() - boundsAfterReshape.getCenterX(), + boundsBeforeReshape.getCenterY() - boundsAfterReshape.getCenterY()); + } + super.notifyChange(evt); + } + + @Override + public void setId(long id){ + super.setId(id); + /* when they are given an id nodes change name into "new <type> node <id>" * + * where <type> is the actual type of the node and <id> is the given id * + * therefore a reshape is necessary to display the new name */ + Rectangle2D boundsBeforeReshape = getBounds(); + reshape(); + /* the reshape might change the bounds, so the shape is translated so that the top-left * + * point is at the same position as before just to keep it more consistent */ + Rectangle2D boundsAfterReshape = getBounds(); + translateImplementation( + new Point2D.Double(), + boundsBeforeReshape.getX() - boundsAfterReshape.getX(), + boundsBeforeReshape.getY() - boundsAfterReshape.getY() + ); + } + + @Override + public boolean contains(Point2D p) { + if (getShape().contains(p)) + return true; + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)) + return true; + return false; + } + + protected void reshape(){ + Pair<List<String>, List<String>> splitPropertyTypes = splitPropertyTypes(); + /* properties displayed internally */ + reshapeInnerProperties(splitPropertyTypes.first); + /* properties displayed externally */ + reshapeOuterProperties(splitPropertyTypes.second); + } + + protected Pair<List<String>, List<String>> splitPropertyTypes(){ + List<String> types = getProperties().getTypes(); + ArrayList<String> insidePropertyTypes = new ArrayList<String>(types.size()); + ArrayList<String> outsidePropertyTypes = new ArrayList<String>(types.size()); + for(String type : types){ + if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside) + insidePropertyTypes.add(type); + else + outsidePropertyTypes.add(type); + } + + return new Pair<List<String>, List<String>> (insidePropertyTypes,outsidePropertyTypes); + } + + protected void reshapeOuterProperties(List<String> outsidePropertyTypes){ + for(String type : outsidePropertyTypes){ + List<PropertyNode> propertyNodes = propertyNodesMap.get(type); + List<String> propertyValues = getProperties().getValues(type); + int diff = propertyNodes.size()-propertyValues.size(); + if(diff > 0) // properties have been removed + for(int i=0; i < diff; i++) + propertyNodes.remove(propertyNodes.size() - 1); + else if(diff < 0){ // properties have been added. We need more properties node. + for(int i=0; i < -diff; i++){ + PropertyNode propertyNode = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType()); + Rectangle2D bounds = getBounds(); + double x = bounds.getCenterX() - bounds.getWidth()/2 - PROP_NODE_DIST; + double y = bounds.getCenterX() - PROP_NODE_DIST * i; + propertyNode.translate(x, y); + propertyNodes.add(propertyNode); + } + } + /* set the text on all the property nodes */ + int i = 0; + for(String text : propertyValues){ + NodeProperties.Modifiers modifiers = getProperties().getModifiers(type); + Set<Integer> viewIndexes = modifiers.getIndexes(i); + ModifierView[] views = new ModifierView[viewIndexes.size()]; + int j =0; + for(Integer I : viewIndexes){ + views[j] = (ModifierView) getProperties().getModifiers(type).getView(modifiers.getTypes().get(I)); + j++; + } + propertyNodes.get(i).setText(text,views); + i++; + } + } + } + + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + /* set the bounds for each multiline string and the resulting bound of the node */ + nameLabel = new MultiLineString(); + nameLabel.setText(getName().isEmpty() ? " " : getName()); + nameLabel.setBold(true); + Rectangle2D r = nameLabel.getBounds(); + + for(int i=0; i<insidePropertyTypes.size();i++){ + propertyLabels[i] = new MultiLineString(); + String propertyType = insidePropertyTypes.get(i); + if(getProperties().getValues(propertyType).size() == 0){ + propertyLabels[i].setText(" "); + }else{ + propertyLabels[i].setJustification(MultiLineString.LEFT); + String[] array = new String[getProperties().getValues(propertyType).size()]; + propertyLabels[i].setText(getProperties().getValues(propertyType).toArray(array), getProperties().getModifiers(propertyType)); + } + r.add(new Rectangle2D.Double(r.getX(),r.getMaxY(),propertyLabels[i].getBounds().getWidth(),propertyLabels[i].getBounds().getHeight())); + } + /* set a gap to uniformly distribute the extra space among property rectangles to reach the minimum bound's height */ + boundsGap = 0; + Rectangle2D.Double minBounds = (Rectangle2D.Double)getMinBounds(); + if(r.getHeight() < minBounds.height){ + boundsGap = minBounds.height - r.getHeight(); + boundsGap /= insidePropertyTypes.size(); + } + r.add(minBounds); //make sure it's at least as big as the minimum bounds + dataDisplayBounds.setFrame(new Rectangle2D.Double(dataDisplayBounds.x,dataDisplayBounds.y,r.getWidth(),r.getHeight())); + } + + /** + Draw the node from the Shape with shadow + @param g2 the graphics context + */ + @Override + public void draw(Graphics2D g2){ + /* draw the external shape */ + Shape shape = getShape(); + if (shape == null) return; + Color oldColor = g2.getColor(); + g2.translate(SHADOW_GAP, SHADOW_GAP); + g2.setColor(SHADOW_COLOR); + g2.fill(shape); + g2.translate(-SHADOW_GAP, -SHADOW_GAP); + g2.setColor(g2.getBackground()); + g2.fill(shape); + g2.setColor(Color.BLACK); + g2.draw(shape); + g2.setColor(oldColor); + + /* if there ain't any property to display inside, then display the name in the middle of the data Display bounds */ + if(!anyInsideProperties()){ + nameLabel.draw(g2, dataDisplayBounds); + }else{ + /* draw name */ + Rectangle2D currentBounds = new Rectangle2D.Double( + dataDisplayBounds.x, + dataDisplayBounds.y, + dataDisplayBounds.getWidth(), + nameLabel.getBounds().getHeight()); + if(drawPropertySeparators){ + Shape oldClip = g2.getClip(); + g2.setClip(getShape()); + g2.draw(new Rectangle2D.Double( + getBounds().getX(), + dataDisplayBounds.y, + getBounds().getWidth(), + nameLabel.getBounds().getHeight()) + ); + g2.setClip(oldClip); + } + nameLabel.draw(g2, currentBounds); + + /* draw internal properties */ + Rectangle2D previousBounds; + for(int i=0;i<propertyLabels.length;i++){ + previousBounds = currentBounds; + currentBounds = new Rectangle2D.Double( + previousBounds.getX(), + previousBounds.getMaxY(), + dataDisplayBounds.getWidth(), + propertyLabels[i].getBounds().getHeight()+boundsGap); + if(drawPropertySeparators){ + Shape oldClip = g2.getClip(); + g2.setClip(getShape()); + g2.draw(new Rectangle2D.Double( + getBounds().getX(), + currentBounds.getY(), + getBounds().getWidth(), + currentBounds.getHeight()) + ); + g2.setClip(oldClip); + } + propertyLabels[i].draw(g2, currentBounds); + } + } + + /* draw external properties */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList){ + pn.draw(g2); + Direction d = new Direction( getBounds().getCenterX() - pn.getCenter().getX(), getBounds().getCenterY() - pn.getCenter().getY()); + g2.draw(new Line2D.Double(pn.getConnectionPoint(d), getConnectionPoint(d.turn(180)))); + } + /* draw visual cue for bookmarks and notes */ + super.draw(g2); + } + + protected Rectangle2D getMinBounds(){ + return (Rectangle2D)minBounds.clone(); + } + + public abstract ShapeType getShapeType(); + + @Override + public void encode(Document doc, Element parent){ + super.encode(doc, parent); + if(getProperties().isEmpty()) + return; + NodeList propTagList = doc.getElementsByTagName(PersistenceManager.PROPERTY); + + /* scan all the PROPERTY tags to add the position tag */ + for(int i = 0 ; i< propTagList.getLength(); i++){ + Element propertyTag = (Element)propTagList.item(i); + Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0); + String type = typeTag.getTextContent(); + + /* a property of another node, continue */ + if(!getProperties().getTypes().contains(type)) + continue; + + if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside) + continue; + + List<String> values = getProperties().getValues(type); + if(values.isEmpty()) + continue; + + NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT); + List<PropertyNode> pnList = propertyNodesMap.get(type); + for(int j=0; j<elementTagList.getLength();j++){ + Element elementTag = (Element)elementTagList.item(j); + Element positionTag = doc.createElement(SimpleShapePrototypePersistenceDelegate.POSITION); + positionTag.setAttribute(PersistenceManager.X, String.valueOf(pnList.get(j).getX())); + positionTag.setAttribute(PersistenceManager.Y, String.valueOf(pnList.get(j).getY())); + elementTag.appendChild(positionTag); + } + } + } + + @Override + public void decode(Document doc, Element nodeTag) throws IOException{ + super.decode(doc, nodeTag); + + NodeList propTagList = nodeTag.getElementsByTagName(PersistenceManager.PROPERTY); + + /* split the property types into internal and external, properties have been set by super.decodeXMLInstance */ + ArrayList<String> insidePropertyTypes = new ArrayList<String>(getProperties().getTypes().size()); + ArrayList<String> outsidePropertyTypes = new ArrayList<String>(getProperties().getTypes().size()); + for(String type : getProperties().getTypes()){ + if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside) + insidePropertyTypes.add(type); + else + outsidePropertyTypes.add(type); + } + + /* set the multi-line string bounds for the properties which are displayed internally */ + reshapeInnerProperties(insidePropertyTypes); + + /* scan all the PROPERTY tags to decode the position tag of the properties which are displayed externally */ + for(int i = 0 ; i< propTagList.getLength(); i++){ + Element propertyTag = (Element)propTagList.item(i); + if(propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null) + throw new IOException(); + Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0); + + String type = typeTag.getTextContent(); + /* (check on whether type exists in the node type definition is done in super.decode */ + + if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside) + continue; + /* this will create external nodes and assign them their position */ + NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT); + List<PropertyNode> pnList = new LinkedList<PropertyNode>(); + for(int j=0; j<elementTagList.getLength();j++){ + Element elementTag = (Element)elementTagList.item(j); + if(elementTag.getElementsByTagName(SimpleShapePrototypePersistenceDelegate.POSITION).item(0) == null || + elementTag.getElementsByTagName(PersistenceManager.VALUE).item(0) == null) + throw new IOException(); + Element positionTag = (Element)elementTag.getElementsByTagName(SimpleShapePrototypePersistenceDelegate.POSITION).item(0); + Element valueTag = (Element)elementTag.getElementsByTagName(PersistenceManager.VALUE).item(0); + double dx,dy; + try{ + dx = Double.parseDouble(positionTag.getAttribute(PersistenceManager.X)); + dy = Double.parseDouble(positionTag.getAttribute(PersistenceManager.Y)); + }catch(NumberFormatException nfe){ + throw new IOException(); + } + PropertyNode pn = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType()); + pn.translate(dx, dy); + pn.setText(valueTag.getTextContent(),null); + pnList.add(pn); + } + propertyNodesMap.put(type, pnList); + /* this will apply the modifier format to the properties */ + reshapeOuterProperties(outsidePropertyTypes); + } + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + dataDisplayBounds.setFrame(dataDisplayBounds.getX() + dx, + dataDisplayBounds.getY() + dy, + dataDisplayBounds.getWidth(), + dataDisplayBounds.getHeight()); + /* translate all the external property nodes */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + pn.translate(dx, dy); + } + + @Override + public Object clone(){ + SimpleShapeNode n = (SimpleShapeNode)super.clone(); + n.propertyLabels = new MultiLineString[propertyLabels.length]; + n.nameLabel = new MultiLineString(); + n.propertyNodesMap = new LinkedHashMap<String, List<PropertyNode>>(); + for(String s : propertyNodesMap.keySet()) + n.propertyNodesMap.put(s, new LinkedList<PropertyNode>()); + n.dataDisplayBounds = (Rectangle2D.Double)dataDisplayBounds.clone(); + return n; + } + + protected boolean anyInsideProperties(){ + boolean propInside = false; + for(String type : getProperties().getTypes()){ + if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside){ + if(!getProperties().getValues(type).isEmpty()){ + propInside = true; + break; + } + } + } + return propInside; + } + + protected Rectangle2D.Double dataDisplayBounds; + protected double boundsGap; + protected boolean drawPropertySeparators = true; + protected MultiLineString[] propertyLabels; + protected MultiLineString nameLabel; + protected Map<String,List<PropertyNode>> propertyNodesMap; + + public static enum ShapeType {Circle, Ellipse, Rectangle, Square, Triangle, Transparent}; + public static enum Position {Inside, Outside}; + + private static final int DEFAULT_WIDTH = 100; + private static final int DEFAULT_HEIGHT = 60; + private static final Rectangle2D.Double minBounds = new Rectangle2D.Double(0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT); + private static final int PROP_NODE_DIST = 50; + + /** + * When properties are configure to appear outside, the values are represented as small nodes + * connected (with a straight line) to the node they belong to. This class represents such + * small nodes. The possible shapes are: triangle, rectangle, square, circle, ellipse and no shape in + * which case only the string with the property value and the line connecting it to the node is shown. + * + */ + protected static class PropertyNode{ + public PropertyNode(ShapeType aShape){ + /* add a little padding in the shape holding the label */ + label = new MultiLineString(){ + public Rectangle2D getBounds(){ + Rectangle2D bounds = super.getBounds(); + if(bounds.getWidth() != 0 || bounds.getHeight() != 0){ + bounds.setFrame( + bounds.getX(), + bounds.getY(), + bounds.getWidth() + PADDING, + bounds.getHeight() + PADDING); + } + return bounds; + } + }; + label.setJustification(MultiLineString.CENTER); + shapeType = aShape; + shape = label.getBounds(); + } + + public void setText(String text, ModifierView[] views){ + label.setText(text,views); + + switch(shapeType){ + case Circle : + Rectangle2D circleBounds = EllipticalNode.getOutBounds(label.getBounds()); + shape = new Ellipse2D.Double( + circleBounds.getX(), + circleBounds.getY(), + Math.max(circleBounds.getWidth(),circleBounds.getHeight()), + Math.max(circleBounds.getWidth(),circleBounds.getHeight()) + ); + break; + case Ellipse : + Rectangle2D ellipseBounds = EllipticalNode.getOutBounds(label.getBounds()); + shape = new Ellipse2D.Double( + ellipseBounds.getX(), + ellipseBounds.getY(), + ellipseBounds.getWidth(), + ellipseBounds.getHeight() + ); + break; + case Triangle : + shape = TriangularNode.getOutShape(label.getBounds()); + break; + default : // Rectangle, Square and Transparent + shape = label.getBounds();; + break; + } + + /* a new shape, placed at (0,0) has been created as a result of set text, therefore * + * we must put it back where the old shape was, since the translation is performed * + * by adding the translate args to x and y, x and y must first be set to 0 */ + double currentX = x; + double currentY = y; + x = 0; + y = 0; + translate(currentX,currentY); + } + + public void draw(Graphics2D g){ + Color oldColor = g.getColor(); + if(shapeType != ShapeType.Transparent){ + g.translate(SHADOW_GAP, SHADOW_GAP); + g.setColor(SHADOW_COLOR); + g.fill(shape); + g.translate(-SHADOW_GAP, -SHADOW_GAP); + + g.setColor(g.getBackground()); + g.fill(shape); + g.setColor(Color.BLACK); + g.draw(shape); + } + + label.draw(g, shape.getBounds2D()); + g.setColor(oldColor); + } + + public void translate(double dx, double dy){ + x += dx; + y += dy; + + if(shape instanceof Path2D){ //it's a triangle + Rectangle2D labelBounds = label.getBounds(); + labelBounds.setFrame( + x, + y, + labelBounds.getWidth(), + labelBounds.getHeight() + ); + shape = TriangularNode.getOutShape(labelBounds); + }else{ + Rectangle2D bounds = shape.getBounds2D(); + ((RectangularShape)shape).setFrame( + x, + y, + bounds.getWidth(), + bounds.getHeight() + ); + } + } + + public Point2D getConnectionPoint(Direction d) { + switch(shapeType){ + case Circle : + case Ellipse : + return EllipticalNode.calculateConnectionPoint(d, shape.getBounds2D()); + case Triangle : + return TriangularNode.calculateConnectionPoint(d, shape.getBounds2D()); + default : + return RectangularNode.calculateConnectionPoint(d, shape.getBounds2D()); + } + } + + public boolean contains(Point2D p){ + return shape.contains(p); + } + + public Point2D getCenter(){ + Rectangle2D bounds = shape.getBounds2D() ; + return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY()); + } + + double getX(){ + return x; + } + + double getY(){ + return y; + } + + private static final int PADDING = 5; + private MultiLineString label; + private ShapeType shapeType; + private Shape shape; + private double x; + private double y; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapePrototypePersistenceDelegate.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,258 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.LineStyle; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; +import uk.ac.qmul.eecs.ccmi.gui.persistence.PrototypePersistenceDelegate; + +/** + * + * A PrototypePersistenceDelegate class which provides informations to be saved and restored by a PersistenceManager + * in order to rebuild simple shaped nodes and edges out of an XML file. + * + */ +public class SimpleShapePrototypePersistenceDelegate implements PrototypePersistenceDelegate{ + + public void encodeNodePrototype(Document doc, Element parent, Node node){ + SimpleShapeNode n = (SimpleShapeNode)node; + Element typeTag = doc.createElement(PersistenceManager.TYPE); + typeTag.appendChild(doc.createTextNode(n.getType())); + parent.appendChild(typeTag); + + Element shapeType = doc.createElement(SHAPE_TYPE); + shapeType.appendChild(doc.createTextNode(n.getShapeType().toString())); + parent.appendChild(shapeType); + + Element propertiesTag = doc.createElement(PersistenceManager.PROPERTIES); + parent.appendChild(propertiesTag); + for(String type : n.getProperties().getTypes()){ + Element propertyTag = doc.createElement(PersistenceManager.PROPERTY); + propertiesTag.appendChild(propertyTag); + + Element propertyTypeTag = doc.createElement(PersistenceManager.TYPE); + propertyTypeTag.appendChild(doc.createTextNode(type)); + propertyTag.appendChild(propertyTypeTag); + + Element viewTag = doc.createElement(VIEW); + propertyTag.appendChild(viewTag); + PropertyView view = (PropertyView)n.getProperties().getView(type); + viewTag.setAttribute(POSITION, view.getPosition().toString()); + viewTag.setAttribute(SHAPE_TYPE, view.getShapeType().toString()); + Modifiers modifiers = n.getProperties().getModifiers(type); + if(!modifiers.isNull()){ + Element modifiersTag = doc.createElement(PersistenceManager.MODIFIERS); + propertyTag.appendChild(modifiersTag); + for(int i=0; i<modifiers.getTypes().size();i++){ + String modifierType = modifiers.getTypes().get(i); + Element modifierTag = doc.createElement(PersistenceManager.MODIFIER); + modifiersTag.appendChild(modifierTag); + modifierTag.setAttribute(PersistenceManager.ID, String.valueOf(i)); + + Element modifierTypeTag = doc.createElement(PersistenceManager.TYPE); + modifierTag.appendChild(modifierTypeTag); + modifierTypeTag.appendChild(doc.createTextNode(modifierType)); + + ModifierView modifierView = (ModifierView)modifiers.getView(modifierType); + viewTag = doc.createElement(VIEW); + modifierTag.appendChild(viewTag); + viewTag.setAttribute(BOLD, modifierView.isBold()+""); + viewTag.setAttribute(UNDERLINE, modifierView.isUnderline()+""); + viewTag.setAttribute(ITALIC, modifierView.isItalic()+""); + viewTag.setAttribute(PREFIX, modifierView.getPrefix()); + viewTag.setAttribute(SUFFIX, modifierView.getSuffix()); + } + } + } + } + + public void encodeEdgePrototype(Document doc, Element parent, Edge edge){ + SimpleShapeEdge e = (SimpleShapeEdge)edge; + Element typeTag = doc.createElement(PersistenceManager.TYPE); + typeTag.appendChild(doc.createTextNode(e.getType())); + parent.appendChild(typeTag); + + Element lineStyleTag = doc.createElement(LINE_STYLE); + lineStyleTag.appendChild(doc.createTextNode(e.getStyle().toString())); + parent.appendChild(lineStyleTag); + + Element minNodesTag = doc.createElement(MIN_ATTACHED_NODES); + parent.appendChild(minNodesTag); + minNodesTag.appendChild(doc.createTextNode(e.getMinAttachedNodes()+"")); + + Element maxNodesTag = doc.createElement(MAX_ATTACHED_NODES); + parent.appendChild(maxNodesTag); + maxNodesTag.appendChild(doc.createTextNode(e.getMaxAttachedNodes()+"")); + + if(e.getHeads() != null) + if(e.getHeads().length != 0){ + Element headsTag = doc.createElement(HEADS); + parent.appendChild(headsTag); + for(int i=0; i<e.getHeads().length; i++){ + ArrowHead head = e.getHeads()[i]; + Element headTag = doc.createElement(HEAD); + headsTag.appendChild(headTag); + headTag.setAttribute(HEAD, head.toString()); + headTag.setAttribute(HEAD_DESCRIPTION, e.getAvailableEndDescriptions()[i]); + } + } + } + + public Node decodeNodePrototype(Element root) throws IOException{ + if(root.getElementsByTagName(PersistenceManager.TYPE).item(0) == null || + root.getElementsByTagName(SHAPE_TYPE).item(0) == null) + throw new IOException(); + + String typeName = root.getElementsByTagName(PersistenceManager.TYPE).item(0).getTextContent(); + String shapeTypeName = root.getElementsByTagName(SHAPE_TYPE).item(0).getTextContent(); + SimpleShapeNode.ShapeType shapeType; + try { + shapeType = SimpleShapeNode.ShapeType.valueOf(shapeTypeName); + }catch(IllegalArgumentException e){ + throw new IOException(); + } + + NodeList propertyTagList = root.getElementsByTagName(PersistenceManager.PROPERTY); + LinkedHashMap<String,Set<String>> propertyTypeDefinition = new LinkedHashMap<String,Set<String>>(); + Map<String, PropertyView> propertyViews = new LinkedHashMap<String, PropertyView>(); + Map<String, Map<String,ModifierView>> modifiersView = new LinkedHashMap<String, Map<String,ModifierView>>(); + + for(int i = 0 ; i< propertyTagList.getLength(); i++){ + Element property = (Element)propertyTagList.item(i); + if(property.getElementsByTagName(PersistenceManager.TYPE).item(0) == null || + property.getElementsByTagName(VIEW).item(0) == null) + throw new IOException(); + + String propertyType = property.getElementsByTagName(PersistenceManager.TYPE).item(0).getTextContent(); + Element viewTag = (Element) property.getElementsByTagName(VIEW).item(0); + viewTag.getAttributes(); + try{ + PropertyView propertyView = new PropertyView( + Enum.valueOf(SimpleShapeNode.Position.class,viewTag.getAttribute(POSITION)), + Enum.valueOf(SimpleShapeNode.ShapeType.class,viewTag.getAttribute(SHAPE_TYPE)) + ); + propertyViews.put(propertyType, propertyView); + }catch(IllegalArgumentException e){ + throw new IOException(e); + } + + NodeList modifierTagList = property.getElementsByTagName(PersistenceManager.MODIFIER); + Set<String> modifierTypeDefinition = null; + /* modifierViewsValue is the Map to be eventually put into the modifierViews as a value */ + /* (can be null), the key being the current property (the for cycle current index one) */ + Map<String,ModifierView> modifierViewsValue = null; + if(modifierTagList.getLength() > 0){ + modifierTypeDefinition = new LinkedHashSet<String>(); + modifierViewsValue = new LinkedHashMap<String,ModifierView>(); + } + for(int j=0; j<modifierTagList.getLength();j++ ){ + Element modifierTag = (Element)modifierTagList.item(j); + if(modifierTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null || + modifierTag.getElementsByTagName(VIEW).item(0) == null) + throw new IOException(); + String modifierType = modifierTag.getElementsByTagName(PersistenceManager.TYPE).item(0).getTextContent(); + modifierTypeDefinition.add(modifierType); + + Element modifierViewTag = (Element) modifierTag.getElementsByTagName(VIEW).item(0); + ModifierView modifierView = new ModifierView( + Boolean.parseBoolean(modifierViewTag.getAttribute(UNDERLINE)), + Boolean.parseBoolean(modifierViewTag.getAttribute(BOLD)), + Boolean.parseBoolean(modifierViewTag.getAttribute(ITALIC)), + modifierViewTag.getAttribute(PREFIX), + modifierViewTag.getAttribute(SUFFIX) + ); + modifierViewsValue.put(modifierType, modifierView); + } + if(modifierTagList.getLength() > 0){ + modifiersView.put(propertyType, modifierViewsValue); + } + propertyTypeDefinition.put(propertyType, modifierTypeDefinition); + } + + /* create the properties and set the views */ + NodeProperties prps = new NodeProperties(propertyTypeDefinition); + for(String propertyType : propertyViews.keySet()){ + prps.setView(propertyType, propertyViews.get(propertyType)); + if(modifiersView.get(propertyType) != null) + for(String modifierType : modifiersView.get(propertyType).keySet()){ + prps.getModifiers(propertyType).setView(modifierType, modifiersView.get(propertyType).get(modifierType)); + } + } + + return SimpleShapeNode.getInstance(shapeType, typeName, prps); + } + + public Edge decodeEdgePrototype(Element root) throws IOException{ + if(root.getElementsByTagName(PersistenceManager.TYPE).item(0) == null || + root.getElementsByTagName(LINE_STYLE).item(0) == null || + root.getElementsByTagName(MIN_ATTACHED_NODES).item(0) == null || + root.getElementsByTagName(MAX_ATTACHED_NODES).item(0) == null) + throw new IOException(); + String typeName = root.getElementsByTagName(PersistenceManager.TYPE).item(0).getTextContent(); + LineStyle lineStyle = null; + try{ + lineStyle = LineStyle.valueOf(root.getElementsByTagName(LINE_STYLE).item(0).getTextContent()); + }catch(IllegalArgumentException e){ + throw new IOException(e); + } + int minAttachedNodes = Integer.parseInt(root.getElementsByTagName(MIN_ATTACHED_NODES).item(0).getTextContent()); + int maxAttachedNodes = Integer.parseInt(root.getElementsByTagName(MAX_ATTACHED_NODES).item(0).getTextContent()); + + NodeList headTagList = root.getElementsByTagName(HEAD); + ArrowHead[] heads = new ArrowHead[headTagList.getLength()]; + String[] headDescriptions = new String[headTagList.getLength()]; + + for(int i=0;i<headTagList.getLength();i++){ + Element headTag = (Element)headTagList.item(i); + heads[i] = ArrowHead.getArrowHeadFromString(headTag.getAttribute(HEAD)); + headDescriptions[i] = headTag.getAttribute(HEAD_DESCRIPTION); + } + return new SimpleShapeEdge(typeName, lineStyle, heads, headDescriptions, minAttachedNodes, maxAttachedNodes); + } + + public static final String SHAPE_TYPE = "ShapeType"; + public static final String VIEW = "View"; + public static final String BOLD = "Bold"; + public static final String ITALIC = "Italic"; + public static final String UNDERLINE = "Underline"; + public static final String PREFIX = "Prefix"; + public static final String SUFFIX = "Suffix"; + public static final String POSITION = "Position"; + public static final String LINE_STYLE = "LineStyle"; + public static final String MIN_ATTACHED_NODES = "MinAttachedNodes"; + public static final String MAX_ATTACHED_NODES = "MaxAttachedNodes"; + public static final String HEADS = "Heads"; + public static final String HEAD = "Head"; + public static final String HEAD_DESCRIPTION = "headLabel"; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleTemplateEditor.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,59 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Frame; +import java.util.Collection; +import java.util.ResourceBundle; + +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.TemplateEditor; + +/** + * + * The implementation of the TemplateEditor interface which uses a Wizard + * to allow the user to define the templates to be created. + * + */ +public class SimpleTemplateEditor implements TemplateEditor { + + @Override + public Diagram createNew(Frame frame, Collection<String> existingTemplates) { + Wizard wizard = new Wizard(frame,existingTemplates); + return wizard.execute(); + } + + @Override + public Diagram edit(Frame frame, Collection<String> existingTemplates, + Diagram diagram) { + Wizard wizard = new Wizard(frame,existingTemplates,diagram); + return wizard.execute(); + } + + @Override + public String getLabelForNew(){ + return ResourceBundle.getBundle(SpeechWizardDialog.class.getName()).getString("wizard_new_label"); + } + + @Override + public String getLabelForEdit(){ + return ResourceBundle.getBundle(SpeechWizardDialog.class.getName()).getString("wizard_edit_label"); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardDialog.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,94 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JRootPane; +import javax.swing.KeyStroke; +import javax.swing.event.ChangeEvent; + +import jwizardcomponent.dialog.SimpleJWizardDialog; + +/* + * The dialog where the template wizard is displayed + * + * @see Wizard + * + */ +@SuppressWarnings("serial") +public class SpeechWizardDialog extends SimpleJWizardDialog { + public SpeechWizardDialog(Frame owner){ + super(owner,true); + finished = false; + + ResourceBundle resources = ResourceBundle.getBundle(getClass().getName()); + setSize(350, 200); + setTitle(resources.getString("dialog.wizard.title")); + setLocationRelativeTo(owner); + + JButton button; + button = getWizardComponents().getNextButton(); + button.setText(resources.getString("button.next.label")); + button.getAccessibleContext().setAccessibleName(resources.getString("button.next.speech")); + + button = getWizardComponents().getBackButton(); + button.setText(resources.getString("button.previous.label")); + button.getAccessibleContext().setAccessibleName(resources.getString("button.previous.speech")); + + button = getWizardComponents().getCancelButton(); + button.setText(resources.getString("button.cancel.label")); + + button = getWizardComponents().getFinishButton(); + button.setText(resources.getString("button.finish.label")); + button.addChangeListener(new javax.swing.event.ChangeListener(){ + @Override + public void stateChanged(ChangeEvent e) { + ((JButton)e.getSource()).setEnabled(finished); + } + }); + JRootPane rootPane = getRootPane(); + rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "close"); + rootPane.getActionMap().put("close", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + dispose(); + } + }); + } + + /** + * Enables or disables the finish button. + * @param enabled {@code true} to enable the button, {@code false} to disable + */ + public void setFinishButtonEnabled(boolean enabled){ + finished = enabled; + getWizardComponents().getFinishButton().setEnabled(true); + } + + private boolean finished; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardDialog.properties Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,99 @@ +wizard_new_label=New Template +wizard_edit_label=Edit Template + +dialog.wizard.title=Template Creation Dialog +dialog.error.existing_value=Error: value {0} already exists +dialog.error.empty_text=Error: text cannot be empty +dialog.error.empty_desc=Error: {0} label cannot be empty +dialog.error.min_max=Error: minimum value cannot be greater than maximum value +dialog.summary.title=Template summary +dialog.summary.ok_button_label=Create Template +dialog.summary.cancel_button_label=Back to Editing +dialog.error.node_type_not_present=Node type {0} not present in template definition +dialog.error.edge_type_not_present=Edge type {0} not present in template definition + + +panel.home.title.new=What would you like to do? +panel.home.choice.diagram_name=Edit Diagram Name +panel.home.choice.nodes=Edit Diagram Nodes +panel.home.choice.edges=Edit Diagram Edges +panel.home.choice.finish=Finish Editing Template + +panel.diagram_name.title=Enter Diagram Name +panel.diagram_name.title.editing_existing_diagram=Enter Diagram Name (different from the initial diagram's) + +panel.nodes.title=Select Action to perform for Nodes +panel.nodes.actions.add=Add New Node +panel.nodes.actions.edit=Edit Existing Node +panel.nodes.actions.del=Delete Existing Node +panel.nodes.actions.finish=Finish Editing Nodes + +panel.node_edit.title=Select Node to edit +panel.node_del.title=Select Node to delete +panel.node_name.title=Enter Node name +panel.node_shape.title=Select Node shape + +panel.yesno_properties.title=Would you like to add any Properties? +panel.yesno_properties.add=Yes +panel.yesno_properties.finish=No + +panel.properties.title=Select Action to perform for Properties +panel.properties.actions.add=Add New Property +panel.properties.actions.edit=Edit Existing Property +panel.properties.actions.del=Delete Existing Property +panel.properties.action.finish=Finish Editing Properties + +panel.property_del.title=Select Property to delete +panel.property_edit.title=Select Property to edit +panel.property_name.title=Enter Property Name +panel.property_shape.title=Select property shape +panel.property_position.title=Where would you like to place the Property ? + +panel.yesno_modifiers.title=Would you like to add any Modifiers? +panel.yesno_modifiers.add=Yes +panel.yesno_modifiers.finish=No + +panel.modifiers.title=Select Action to perform for Modifiers +panel.modifiers.actions.add=Add New Modifier +panel.modifiers.actions.edit=Edit Existing Modifier +panel.modifiers.actions.del=Delete Existing Modifier +panel.modifiers.actions.finish=Finish Editing Modifiers + +panel.modifier_type.title=Enter Modifier Name +panel.modifier_del.title=Select Modifier to Delete +panel.modifier_edit.title=Select Modifier to Edit +panel.modifier_format.title=Select Modifier Format + +panel.edges.title=Select Action to perform for Edges +panel.edges.actions.add=Add New Edge +panel.edges.actions.edit=Edit Existing Edge +panel.edges.actions.del=Delete Existing Edge +panel.edges.actions.finish=Finish Editing Edges + +panel.edge_name.title=Enter Edge Name +panel.edge_del.title=Select Edge to Delete +panel.edge_edit.title=Select Edge to Edit +panel.edge_linestyle.title=Select Edge Line Style +panel.edge_min_nodes.title=Select Minimum Nodes To Connect +panel.edge_max_nodes.title=Select Maximum Nodes To Connect +panel.edge_yesno_arrow_head.title=Would you like to add arrow heads? +panel.edge_yesno_arrow_head.actions.add=Yes +panel.edge_yesno_arrow_head.actions.finish=No +panel.edge_arrow_head.title=Select arrow heads and set their labels + +modifier.format.bold=Bold +modifier.format.underline=Underline +modifier.format.italic=Italic +modifier.format.prefix=Prefix +modifier.format.suffix=Suffix + +button.next.label=Next > +button.next.speech=Next +button.previous.label=< Back +button.previous.speech=Back +button.finish.label=Done +button.cancel.label=Cancel + + +file={0}, File +dir={0}, Directory \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SpeechWizardPanel.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,319 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.ResourceBundle; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import jwizardcomponent.JWizardComponents; +import jwizardcomponent.JWizardPanel; +import uk.ac.qmul.eecs.ccmi.gui.LoopComboBox; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; + +/* + * The abstract class providing basic implementation for the panels displayed when the template + * wizard is run in order to build a diagram template. Subclasses will define the central component + * displayed in the panel. The central component is an input component (e.g. a JTextField), + * through which the user enters the input required for at that particular step of the wizard. + * + * + * @see Wizard + */ +@SuppressWarnings("serial") +abstract class SpeechWizardPanel extends JWizardPanel { + public SpeechWizardPanel(JWizardComponents wizardComponents, String title, int next, int previous){ + super(wizardComponents,title); + label = new JLabel(title); + this.next = next; + this.previous = previous; + } + + @Override + public void update(){ + Component focusOwner = assignFocus(); + NarratorFactory.getInstance().speak( + new StringBuilder(getPanelTitle()) + .append(' ') + .append(SpeechUtilities.getComponentSpeech(focusOwner)).toString()); + super.update(); + } + + @Override + public void setPanelTitle(String title){ + label.setText(title); + super.setPanelTitle(title); + } + + protected Component assignFocus(){ + if(component != null) + component.requestFocus(); + return component; + } + + /** + * Lays out the components according to the layout manager. This method is used by subclasses + * by passing the component the user use for input (e.g. a text field or a combo-box) as argument. + * such component is placed at the centre of the panel above the buttons. + * @param centralComponent the component to be laid out at the centre dialog + */ + protected void layoutComponents(JComponent centralComponent){ + component = centralComponent; + /* pressing enter on the central component results in a switch to the next panel */ + component.addKeyListener(new KeyAdapter(){ + @Override + public void keyPressed(KeyEvent evt){ + pressed = true; + } + + @Override + public void keyTyped(KeyEvent evt){ + /* switch on the next panel only if the press button started on the same window * + * this is to avoid keyTyped to be called after the panel switch and therefore refer * + * to a component different that the one the user pressed OK on */ + if(evt.getKeyChar() == '\n' && pressed) + getWizardComponents().getNextButton().doClick(); + pressed = false; + } + boolean pressed = false; + }); + + GridBagConstraints constr = new GridBagConstraints(); + constr.gridx = 0; + constr.gridy = 0; + constr.gridwidth = 1; + constr.gridheight = 1; + constr.weightx = 1.0; + constr.weighty = 0.0; + constr.anchor = GridBagConstraints.PAGE_START; + constr.fill = GridBagConstraints.BOTH; + constr.insets = new Insets(5, 5, 5, 5); + constr.ipadx = 0; + constr.ipady = 0; + + /* Label */ + setLayout(new GridBagLayout()); + JPanel labelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + label.setHorizontalAlignment(SwingConstants.LEADING); + labelPanel.add(label); + add(labelPanel,constr); + + /* JSeparator */ + constr.gridy = 1; + constr.anchor = GridBagConstraints.WEST; + constr.fill = GridBagConstraints.BOTH; + constr.insets = new Insets(1, 1, 1, 1); + add(new JSeparator(), constr); + + /* central component */ + Container centralComponentContainer; + if(centralComponent instanceof JScrollPane ){ + centralComponentContainer = centralComponent; + }else{ + centralComponentContainer = new JPanel(new GridBagLayout()); + centralComponentContainer.add(centralComponent + , new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0 + , GridBagConstraints.CENTER, GridBagConstraints.BOTH + , new Insets(0, 0, 0, 0), 0, 0)); + } + constr.gridy = 2; + constr.weighty = 1.0; + constr.anchor = GridBagConstraints.CENTER; + constr.insets = new Insets(0, 0, 0, 0); + add(centralComponentContainer,constr); + } + + @Override + public void next(){ + switchPanel(next); + } + + @Override + public void back(){ + switchPanel(previous); + } + + private JLabel label; + private int next; + private int previous; + private JComponent component; + public static int OWN_SWITCH = -1; + public static int DISABLE_SWITCH = -2; +} + +@SuppressWarnings("serial") +class SelectWizardPanel extends SpeechWizardPanel { + SelectWizardPanel(JWizardComponents wizardComponents, String title, Collection<String> options, int next, int previous, Model.Record record){ + super(wizardComponents,title,next,previous); + String[] optionsArray = new String[options.size()]; + comboBox = new LoopComboBox(new DefaultComboBoxModel(options.toArray(optionsArray))); + comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + layoutComponents(comboBox); + this.record = record; + } + + SelectWizardPanel(JWizardComponents wizardComponents, String title, Collection<String> options, int[] nexts, int previous, Model.Record record){ + this(wizardComponents, title, options, OWN_SWITCH, previous, record); + this.nexts = nexts; + } + + SelectWizardPanel(JWizardComponents wizardComponents, String title, Collection<String> options, int[] nexts, int previous){ + this(wizardComponents, title, options, nexts, previous,null); + } + + @Override + public void next(){ + if(record != null) + record.value = (String)comboBox.getSelectedItem(); + if(nexts != null) + switchPanel(nexts[comboBox.getSelectedIndex()]); + else + super.next(); + } + + @Override + public void update(){ + if(record != null) + comboBox.setSelectedItem(record.value); + super.update(); + } + + JComboBox comboBox; + int[] nexts; + Model.Record record; +} + +@SuppressWarnings("serial") +class TextWizardPanel extends SpeechWizardPanel { + TextWizardPanel(JWizardComponents wizardComponents, String title, Collection<String> existingValues, int next, int previous, Model.Record record){ + super(wizardComponents,title,next,previous); + textField = new JTextField(); + textField.setColumns(10); + textField.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + layoutComponents(textField); + this.record = record; + this.existingValues = existingValues; + } + + public void next(){ + String text = textField.getText().trim(); + /* if the user enters a text he has already entered (that is, it's in the existingValues the don't go on */ + /* and notify the user they have to chose another text. The only exception is when the record contains */ + /* the same text the user entered as that means they are going through the editing of an existing element*/ + if(text.isEmpty()||"\n".equals(text)){ + NarratorFactory.getInstance().speak(ResourceBundle.getBundle(SpeechWizardDialog.class.getName()).getString("dialog.error.empty_text")); + return; + } + for(String value : existingValues){ + if(value.equals(text) && !text.equals(record.value)){ + NarratorFactory.getInstance().speak(MessageFormat.format( + ResourceBundle.getBundle(SpeechWizardDialog.class.getName()).getString("dialog.error.existing_value"), + text)); + return; + } + } + if(record != null) + record.value = text; + super.next(); + } + + @Override + public void update(){ + if(record != null) + textField.setText(record.value); + super.update(); + } + + JTextField textField; + Collection<String> existingValues; + Model.Record record; +} + +@SuppressWarnings("serial") +class SpinnerWizardPanel extends SpeechWizardPanel{ + public SpinnerWizardPanel(JWizardComponents wizardComponents, String title, SpinnerModel spinnerModel, int next, int previous, Model.Record record){ + super(wizardComponents,title,next,previous); + this.record = record; + spinner = new JSpinner(spinnerModel); + spinner.addChangeListener(new ChangeListener(){ + @Override + public void stateChanged(ChangeEvent evt) { + JSpinner s = (JSpinner)(evt.getSource()); + NarratorFactory.getInstance().speak(s.getValue().toString()); + } + }); + JFormattedTextField tf = ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField(); + tf.setEditable(false); + tf.setFocusable(false); + tf.setBackground(Color.white); + layoutComponents(spinner); + } + + @Override + public void next(){ + if(record != null) + record.value = spinner.getValue().toString(); + super.next(); + } + + @Override + public void update(){ + if(record != null){ + if(!record.value.isEmpty()) + spinner.setValue(Integer.parseInt(record.value)); + } + super.update(); + } + + Model.Record record; + JSpinner spinner; +} + +@SuppressWarnings("serial") +class DummyWizardPanel extends JWizardPanel{ + DummyWizardPanel(JWizardComponents wizardComponents){ + super(wizardComponents); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SquareNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,115 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.InputStream; +import java.util.List; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + * A squared shaped diagram node. + * + */ +@SuppressWarnings("serial") +public class SquareNode extends RectangularNode { + + public SquareNode(String nodeType, NodeProperties properties){ + super(nodeType, properties); + dataDisplayBounds = getMinBounds(); + sqShape = getMinBounds(); + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + super.reshapeInnerProperties(insidePropertyTypes); + + double diffwh = dataDisplayBounds.getWidth() - dataDisplayBounds.getHeight(); + if(diffwh > 0){ + sqShape.setFrame(dataDisplayBounds.getX(),dataDisplayBounds.getY()-diffwh/2,dataDisplayBounds.getWidth(),dataDisplayBounds.getWidth()); + } else if(diffwh < 0){ + sqShape.setFrame(dataDisplayBounds.getX()+diffwh/2,dataDisplayBounds.getY(),dataDisplayBounds.getHeight(),dataDisplayBounds.getHeight()); + }else{ + sqShape.setFrame(dataDisplayBounds); + } + } + + public Rectangle2D.Double getMinBounds(){ + Rectangle2D r = super.getMinBounds(); + r.setFrame(r.getX(), r.getY(), r.getHeight(), r.getHeight()); + return (Rectangle2D.Double)r; + } + + @Override + public ShapeType getShapeType(){ + return ShapeType.Square; + } + + @Override + public InputStream getSound(){ + return sound; + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + + sqShape.setFrame(sqShape.getX() + dx, + sqShape.getY() + dy, + sqShape.getWidth(), + sqShape.getHeight()); + super.translateImplementation(p,dx, dy); + } + + @Override + public Rectangle2D getBounds(){ + return (Rectangle2D)sqShape.clone(); + } + + @Override + public Shape getShape(){ + return sqShape; + } + + @Override + public Object clone(){ + SquareNode n = (SquareNode)super.clone(); + n.sqShape = (Rectangle2D.Double)sqShape.clone(); + return n; + } + private Rectangle2D.Double sqShape; + private static InputStream sound; + + static{ + sound = SquareNode.class.getResourceAsStream("audio/Square.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/TriangularNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,213 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.Rectangle2D.Double; +import java.io.InputStream; +import java.util.List; + +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.gui.Direction; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + * A triangular shaped diagram node. + * + */ +@SuppressWarnings("serial") +public class TriangularNode extends SimpleShapeNode { + + + public TriangularNode(String typeName, NodeProperties properties) { + super(typeName, properties); + Rectangle2D dataBounds = getMinBounds(); + dataDisplayBounds.setFrame(dataBounds); + tShape = getOutShape(dataBounds); + /* by building the shape around dataBounds which was at (0,0) the new bounds */ + /* are now negative, so we need to bring the new bounds back at (0,0) */ + Rectangle2D bounds = getBounds(); + translateImplementation(new Point2D.Double(),0-bounds.getX(),0-bounds.getY()); + } + + @Override + protected Rectangle2D getMinBounds(){ + Rectangle2D minBounds = super.getMinBounds(); + return new Rectangle2D.Double(minBounds.getX(),minBounds.getY(),minBounds.getWidth()/2,minBounds.getHeight()/2); + } + + @Override + public ShapeType getShapeType() { + return ShapeType.Triangle; + } + + @Override + protected void translateImplementation(Point2D p, double dx, double dy){ + /* if we clicked on a property node, just move that one */ + for(List<PropertyNode> pnList : propertyNodesMap.values()) + for(PropertyNode pn : pnList) + if(pn.contains(p)){ + pn.translate(dx, dy); + return; + } + super.translateImplementation(p,dx, dy); + tShape.transform(AffineTransform.getTranslateInstance(dx, dy)); + } + + public static Path2D.Double getOutShape(Rectangle2D r){ + Path2D.Double triangle = new Path2D.Double(GeneralPath.WIND_EVEN_ODD,3); + double minEdge = Math.min(r.getWidth(), r.getHeight()); + triangle.moveTo(r.getCenterX(), r.getY()-minEdge); + + double angle = Math.atan(minEdge/(r.getWidth()/2)); + double w = r.getHeight()/ Math.tan(angle); + triangle.lineTo(r.getX()-w, r.getMaxY()); + triangle.lineTo(r.getMaxX()+w, r.getMaxY()); + triangle.closePath(); + return triangle; + } + + @Override + protected void reshapeInnerProperties(List<String> insidePropertyTypes){ + nameLabel = new MultiLineString(); + nameLabel.setText(getName().isEmpty() ? " " : getName()); + nameLabel.setBold(true); + + if(!super.anyInsideProperties()){ + dataDisplayBounds.setFrame(dataDisplayBounds.getX(), + dataDisplayBounds.getY(), + nameLabel.getBounds().getWidth(), + nameLabel.getBounds().getHeight()); + Rectangle2D minBounds = getMinBounds(); + dataDisplayBounds.add(new Rectangle2D.Double(dataDisplayBounds.getX(), dataDisplayBounds.getY(), minBounds.getWidth(),minBounds.getHeight())); + tShape = getOutShape(dataDisplayBounds); + }else { + Rectangle2D r = nameLabel.getBounds(); + + for(int i=0; i<insidePropertyTypes.size();i++){ + propertyLabels[i] = new MultiLineString(); + if(getProperties().getValues(insidePropertyTypes.get(i)).size() == 0){ + propertyLabels[i].setText(" "); + }else{ + propertyLabels[i].setJustification(MultiLineString.LEFT); + String[] a = new String[getProperties().getValues(insidePropertyTypes.get(i)).size()]; + propertyLabels[i].setText(getProperties().getValues(insidePropertyTypes.get(i)).toArray(a), getProperties().getModifiers(insidePropertyTypes.get(i))); + } + r.add(new Rectangle2D.Double(r.getX(),r.getMaxY(),propertyLabels[i].getBounds().getWidth(),propertyLabels[i].getBounds().getHeight())); + } + /* set a gap to uniformly distribute the extra space among property rectangles to reach the minimum bound's height */ + boundsGap = 0; + Rectangle2D.Double minBounds = (Rectangle2D.Double)getMinBounds(); + if(r.getHeight() < minBounds.height){ + boundsGap = minBounds.height - r.getHeight(); + boundsGap /= insidePropertyTypes.size(); + } + r.add(minBounds); + dataDisplayBounds.setFrame(new Rectangle2D.Double(dataDisplayBounds.x,dataDisplayBounds.y,r.getWidth(),r.getHeight())); + + tShape = getOutShape(dataDisplayBounds); + } + + } + + @Override + public Double getBounds() { + return (Double)tShape.getBounds2D(); + } + + @Override + public InputStream getSound(){ + return sound; + } + + @Override + public Point2D getConnectionPoint(Direction d) { + return calculateConnectionPoint(d,getBounds()); + } + + public static Point2D calculateConnectionPoint(Direction d, Rectangle2D bounds) { + if(d.getX() == 0){ + return new Point2D.Double(bounds.getCenterX(), + d.getY() > 0 ? bounds.getY() : bounds.getMaxY()); + } + + boolean left = false; + boolean right = false; + double dirTan = d.getY()/d.getX(); + double boundsTan = bounds.getHeight()/bounds.getWidth(); + double alfa = Math.atan(dirTan); + double alfaDegrees = Math.toDegrees(alfa); + + if(d.getY() < 0){ //from the top + if(alfaDegrees < 0) + right = true; + else + left = true; + }else{ //from the bottom + if(dirTan < boundsTan && d.getX() > 0) + right = true; + else if(dirTan > -boundsTan && d.getX() < 0) + left = true; + } + + if(right){ + double beta = Math.atan(bounds.getHeight()/(bounds.getWidth()/2)); + double py = bounds.getHeight()/2; + double x = py/ (Math.tan(alfa)-Math.tan(beta)); + double y = x * Math.tan(alfa); + return new Point2D.Double(bounds.getCenterX()-x, bounds.getCenterY()-y); + } + else if(left){ + double beta = - Math.atan(bounds.getHeight()/(bounds.getWidth()/2)); + double py = bounds.getHeight()/2; + double x = py/ (Math.tan(alfa)-Math.tan(beta)); + double y = x * Math.tan(alfa); + return new Point2D.Double(bounds.getCenterX()-x, bounds.getCenterY()-y); + } + else{ + return new Point2D.Double( + bounds.getCenterX() + ((bounds.getHeight()/2) * (d.getX()/d.getY()) ), + bounds.getMaxY()); + } + } + + @Override + public Shape getShape() { + return tShape; + } + + public Object clone(){ + return new TriangularNode(getType(),(NodeProperties)getProperties().clone()); + } + + private Path2D.Double tShape; + private static InputStream sound; + + static { + sound = TriangularNode.class.getResourceAsStream("audio/Triangle.mp3"); + SoundFactory.getInstance().loadSound(sound); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/Wizard.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,1210 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.simpletemplate; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.ResourceBundle; +import java.util.Set; + +import javax.swing.AbstractAction; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SpinnerModel; + +import jwizardcomponent.FinishAction; +import jwizardcomponent.JWizardComponents; +import jwizardcomponent.JWizardPanel; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; +import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; +import uk.ac.qmul.eecs.ccmi.gui.Diagram; +import uk.ac.qmul.eecs.ccmi.gui.Edge; +import uk.ac.qmul.eecs.ccmi.gui.LineStyle; +import uk.ac.qmul.eecs.ccmi.gui.LoopComboBox; +import uk.ac.qmul.eecs.ccmi.gui.LoopSpinnerNumberModel; +import uk.ac.qmul.eecs.ccmi.gui.Node; +import uk.ac.qmul.eecs.ccmi.gui.SpeechSummaryPane; +import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapeNode.ShapeType; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; +import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; +import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; +import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; + +/** + * + * A Wizard-like sequence of screens prompted to the user to let they input (e.g. which shape a node will have or how many nodes an edge can connect at most) + * how to build a template diagram. A template diagram is a prototype diagram + * (containing prototype nodes and edges) which can later on be used for creating instances + * of that type of diagram through clonation. The wizard is completely accessible via audio + * as all the content and all focused components names are spoken out by the {@code Narrator} through a text to speech synthesizer. + * + */ +public class Wizard { + public Wizard(Frame frame, Collection<String> existingDiagrams, Diagram diagramToEdit){ + dialog = new SpeechWizardDialog(frame); + resources = ResourceBundle.getBundle(SpeechWizardDialog.class.getName()); + + model = createModel(diagramToEdit); + node = new Model.Node(); + edge = new Model.Edge(); + property = new Model.Property(); + modifier = new Model.Modifier(); + + initWizardComponents(existingDiagrams,diagramToEdit); + + /* if the user is editing from an existing diagram, they have to choose a new name. They're switched * + * directly to the diagram name panel so they have to enter a new name as they would otherwise * + * not be allowed to proceed. */ + if(diagramToEdit != null) + dialog.getWizardComponents().setCurrentIndex(DIAGRAM_NAME); + + /* when the user clicks on the finish button they'll be prompted with a summary text area dialog * + * describing what they have created so far and asking for a confirmation to proceed with the actual * + * creation of the template. */ + dialog.getWizardComponents().setFinishAction(new FinishAction(dialog.getWizardComponents()){ + @Override + public void performAction(){ + String[] options = { + resources.getString("dialog.summary.ok_button_label"), + resources.getString("dialog.summary.cancel_button_label")}; + int result = SpeechSummaryPane.showDialog( + dialog, + resources.getString("dialog.summary.title"), + model.toString(), + SpeechSummaryPane.OK_CANCEL_OPTION, + options); + + if(result == SpeechSummaryPane.CANCEL){ // user wants to continue editing + /* null arg will avoid default playerListener which speaks out the focused component */ + SoundFactory.getInstance().play(SoundEvent.CANCEL,null); + return; + } + /* create the actual diagram (it will be return to the client class by execute()) */ + diagram = createDiagram(model); + dialog.dispose(); + } + }); + } + + public Wizard(Frame frame, Collection<String> existingDiagrams){ + this(frame,existingDiagrams,null); + } + + public Diagram execute(){ + diagram = null; + dialog.show(); + if(diagram == null) + SoundFactory.getInstance().play(SoundEvent.CANCEL); + return diagram; + } + + @SuppressWarnings("serial") + private void initWizardComponents(Collection<String> existingDiagrams, final Diagram diagramToEdit){ + /* --- MAIN PANEL --- */ + String[] choices = { + resources.getString("panel.home.choice.diagram_name"), + resources.getString("panel.home.choice.nodes"), + resources.getString("panel.home.choice.edges"), + }; + int[] nexts = {DIAGRAM_NAME,NODES,EDGES}; + + /* panel for the selection of main tasks when creating the diagram: enter diagram name, create node and * + * create edge. When a name is assigned an item is added to the selection which allows the user to finish * + * the template creation, much as they would do by pressing the finish button. If the user edits an existing * + * diagram they're prompted with a message to enter a new diagram name (as there cannot be two diagrams * + * with the same name. When the user enters the name the message goes away */ + add(HOME,new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.home.title.new"), + Arrays.asList(choices), + nexts, + SpeechWizardPanel.DISABLE_SWITCH + ){ + @Override + public void update(){ + if(!model.diagramName.value.isEmpty()){ + dialog.setFinishButtonEnabled(true); + /* if the diagram has a name the template creation can finish. So add a selection item to the * + * comboBox unless it's already there from a previous update (item count < 4 ) */ + if(comboBox.getItemCount() < 4) + ((DefaultComboBoxModel)comboBox.getModel()).addElement(resources.getString("panel.home.choice.finish")); + } + super.update(); + } + @Override + public void next(){ + if(comboBox.getSelectedIndex() == 3) + dialog.getWizardComponents().getFinishButton().doClick(); + else + super.next(); + } + }); + + /* --- DIAGRAM NAME INPUT PANEL --- */ + add(DIAGRAM_NAME, new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString(diagramToEdit == null ? "panel.diagram_name.title" : "panel.diagram_name.title.editing_existing_diagram"), + existingDiagrams, + HOME, + HOME, + model.diagramName + ){ + @Override + public void update(){ + /* this is a little nasty trick to achieve the following: when the user creates a new diagram out of an already existing + * one they're directly prompted with this panel. We want the name of the diagram to be there for awareness + * but at the same time it must not be accepted by the program as it would otherwise clash with + * with the starting diagam's. As the program accepts it when the text entered in the text field is equal to + * model.diagramName.value (for when the user wants to re-edit the name of a diagram they're creating) we must + * fill model.DiagramName.value with the name of the starting diagram to get it shown and spoken out but then + * it's assigned the empty string not to let the user to go forward */ + if(diagramToEdit != null && model.diagramName.value.isEmpty()){ + model.diagramName.value = diagramToEdit.getName(); + super.update(); + model.diagramName.value = ""; + }else{ + super.update(); + } + } + }); + + /* --- NODE ACTION SELECTION PANEL --- */ + /* decide whether to add a new node or to edit/delete an existing node */ + String[] nodeOptions = { + resources.getString("panel.nodes.actions.add"), + resources.getString("panel.nodes.actions.edit"), + resources.getString("panel.nodes.actions.del"), + resources.getString("panel.nodes.actions.finish")}; + int[] nodeNexts = {NODE_TYPE,NODE_EDIT,NODE_DEL,HOME}; + add(NODES, new ActionChooserPanel( + dialog.getWizardComponents(), + model.nodes.getNames(), + resources.getString("panel.nodes.title"), + nodeOptions, + nodeNexts, + HOME, + node + )); + + /* --- NODE TYPE NAME INPUT PANEL --- */ + add(NODE_TYPE,new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.node_name.title"), + model.nodes.getNames(), + NODE_SHAPE, + NODES, + node.type + )); + + /* --- NODE TO DELETE SELECTION PANEL*/ + add(NODE_DEL, new DeletePanel( + dialog.getWizardComponents(), + resources.getString("panel.node_del.title"), + NODES, + NODES, + model.nodes)); + + /* -- NODE TO EDIT SELECTION PANEL */ + add(NODE_EDIT, new EditPanel( + dialog.getWizardComponents(), + resources.getString("panel.node_edit.title"), + NODE_TYPE, + NODES, + model.nodes, + node + )); + + ShapeType[] shapeTypes = ShapeType.values(); + ArrayList<String> shapeTypeNames = new ArrayList<String>(shapeTypes.length); + for(int i=0; i<shapeTypes.length;i++) + if(shapeTypes[i] != ShapeType.Transparent) + shapeTypeNames.add(shapeTypes[i].toString()); + + /* -- NODE SHAPE SELECTION PANEL --- */ + add(NODE_SHAPE, new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.node_shape.title"), + shapeTypeNames, + NODE_YESNO_PROPERTIES, + NODE_TYPE, + node.shape + )); + + /* --- SELECT WHETHER THE THE NODE HAS TO HAVE PROPERTIES --- */ + String[] yesnoPropertyOptions = { + resources.getString("panel.yesno_properties.add"), + resources.getString("panel.yesno_properties.finish") + }; + int[] yesnoPropertyNexts = {PROPERTIES,NODES}; + + add(NODE_YESNO_PROPERTIES,new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.yesno_properties.title"), + Arrays.asList(yesnoPropertyOptions), + yesnoPropertyNexts, + NODE_SHAPE + ){ + @Override + public void next(){ + if(comboBox.getSelectedIndex() == 1){ + Model.Node newNode = new Model.Node(); + Model.copy(node, newNode); + model.nodes.put(newNode.id,newNode); + } + super.next(); + } + }); + + /* --- PROPERTIES ACTION SELECTION PANEL --- */ + String[] propertyOptions = { + resources.getString("panel.properties.actions.add"), + resources.getString("panel.properties.actions.edit"), + resources.getString("panel.properties.actions.del"), + resources.getString("panel.properties.action.finish")}; + int[] propertyNexts = {PROPERTY_TYPE,PROPERTY_EDIT,PROPERTY_DEL,NODES}; + + add(PROPERTIES, new ActionChooserPanel( + dialog.getWizardComponents(), + node.properties.getNames(), + resources.getString("panel.properties.title"), + propertyOptions, + propertyNexts, + NODE_SHAPE, + property + ){ + @Override + public void next(){ + /* if the user selects finish, create a new node put in it the values of */ + /* the temporary property and store it in the model */ + if( (comboBox.getSelectedIndex() == 1 && comboBox.getItemCount() == 2)|| + (comboBox.getSelectedIndex() == 3 && comboBox.getItemCount() == 4)){ + Model.Node newNode = new Model.Node(); + Model.copy(node, newNode); + model.nodes.put(newNode.id,newNode); + } + super.next(); + } + }); + + /* --- PROPERTY TYPE NAME INPUT PANEL --- */ + add(PROPERTY_TYPE,new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.property_name.title"), + node.properties.getNames(), + PROPERTY_POSITION, + PROPERTIES, + property.type + )); + + /* --- PROPERTY TO DELETE SELECTION PANEL --- */ + add(PROPERTY_DEL, new DeletePanel( + dialog.getWizardComponents(), + resources.getString("panel.property_del.title"), + PROPERTIES, + PROPERTIES, + node.properties + )); + + /* --- PROPERTY TO EDIT SELECTION PANEL --- */ + add(PROPERTY_EDIT, new EditPanel( + dialog.getWizardComponents(), + resources.getString("panel.property_edit.title"), + PROPERTY_TYPE, + PROPERTIES, + node.properties, + property + )); + + /* --- PROPERTY POSITION SELECTION DIALOG --- */ + SimpleShapeNode.Position positions[] = SimpleShapeNode.Position.values(); + ArrayList<String> positionNames = new ArrayList<String>(positions.length); + for(int i=0; i<positions.length;i++) + positionNames.add(positions[i].toString()); + int[] positionNexts = {PROPERTY_YESNO_MODIFIER,PROPERTY_SHAPE}; + + + add(PROPERTY_POSITION, new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.property_position.title"), + positionNames, + positionNexts, + PROPERTY_TYPE, + property.position + )); + + /* --- PROPERTY SHAPE SELECTION DIALOG --- */ + shapeTypeNames.add(ShapeType.Transparent.toString()); + add(PROPERTY_SHAPE, new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.property_shape.title"), + shapeTypeNames, + PROPERTY_YESNO_MODIFIER, + PROPERTY_POSITION, + property.shape + )); + + /* --- SELECT WHETHER THE THE PROPERTY HAS TO HAVE MODIFIERS --- */ + String[] yesnoModifierOptions = { + resources.getString("panel.yesno_modifiers.add"), + resources.getString("panel.yesno_modifiers.finish") + }; + int[] yesnoModifierNexts = {MODIFIERS,PROPERTIES}; + + add(PROPERTY_YESNO_MODIFIER,new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.yesno_modifiers.title"), + Arrays.asList(yesnoModifierOptions), + yesnoModifierNexts, + PROPERTY_POSITION + ){ + @Override + public void next(){ + if(comboBox.getSelectedIndex() == 1){ + Model.Property newProperty = new Model.Property(); + Model.copy(property, newProperty); + node.properties.put(newProperty.id,newProperty); + } + super.next(); + } + }); + /* --- MODIFIERS ACTION SELECTION PANE --- */ + String[] modifierOptions = { + resources.getString("panel.modifiers.actions.add"), + resources.getString("panel.modifiers.actions.edit"), + resources.getString("panel.modifiers.actions.del"), + resources.getString("panel.modifiers.actions.finish") + }; + int[] modifiersNexts = {MODIFIER_TYPE,MODIFIER_EDIT,MODIFIER_DEL,PROPERTIES}; + + add(MODIFIERS, new ActionChooserPanel( + dialog.getWizardComponents(), + property.modifiers.getNames(), + resources.getString("panel.modifiers.title"), + modifierOptions, + modifiersNexts, + PROPERTY_POSITION, + modifier){ + + @Override + public void next(){ + /* if the user selects finish, create a new property put in it the values of */ + /* the temporary property and store it in the model */ + if( (comboBox.getSelectedIndex() == 1 && comboBox.getItemCount() == 2)|| + (comboBox.getSelectedIndex() == 3 && comboBox.getItemCount() == 4)){ + Model.Property newProperty = new Model.Property(); + Model.copy(property, newProperty); + node.properties.put(newProperty.id,newProperty); + } + super.next(); + } + }); + + /* --- MODIFIER TYPE PANEL --- */ + add(MODIFIER_TYPE, new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.modifier_type.title"), + property.modifiers.getNames(), + MODIFIER_FORMAT, + MODIFIERS, + modifier.type + )); + + add(MODIFIER_DEL, new DeletePanel( + dialog.getWizardComponents(), + resources.getString("panel.modifier_del.title"), + MODIFIERS, + MODIFIERS, + property.modifiers)); + + add(MODIFIER_EDIT, new EditPanel( + dialog.getWizardComponents(), + resources.getString("panel.modifier_edit.title"), + MODIFIER_TYPE, + MODIFIERS, + property.modifiers, + modifier + )); + + add(MODIFIER_FORMAT, new FormatWizardPanel()); + /* --- EDGE ACTION SELECTION PANEL --- */ + /* decide whether to add a new edge or to edit/delete an existing edge */ + String[] edgeOptions = { + resources.getString("panel.edges.actions.add"), + resources.getString("panel.edges.actions.edit"), + resources.getString("panel.edges.actions.del"), + resources.getString("panel.edges.actions.finish")}; + int[] edgeNexts = {EDGE_TYPE,EDGE_EDIT,EDGE_DEL,HOME}; + add(EDGES, new ActionChooserPanel( + dialog.getWizardComponents(), + model.edges.getNames(), + resources.getString("panel.edges.title"), + edgeOptions, + edgeNexts, + HOME, + edge + )); + + /* --- EDGE TYPE NAME INPUT PANEL --- */ + add(EDGE_TYPE,new TextWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_name.title"), + model.edges.getNames(), + EDGE_LINE_STYLE, + EDGES, + edge.type + )); + + /* --- EDGE TO DELETE SELECTION PANEL --- */ + add(EDGE_DEL, new DeletePanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_del.title"), + EDGES, + EDGES, + model.edges)); + + /* --- EDGE TO EDIT SELECTION PANEL --- */ + add(EDGE_EDIT, new EditPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_edit.title"), + EDGE_TYPE, + EDGES, + model.edges, + edge + )); + + /* --- LINE STYLE SELECTION PANEL --- */ + LineStyle[] lineStyles = LineStyle.values(); + String[] lineStyleNames = new String[lineStyles.length]; + for(int i=0; i<lineStyles.length;i++) + lineStyleNames[i] = lineStyles[i].toString(); + + add(EDGE_LINE_STYLE, new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_linestyle.title"), + Arrays.asList(lineStyleNames), + EDGE_MIN_NODES, + EDGE_TYPE, + edge.lineStyle + )); + + /* --- MIN NODES SELECTION PANEL --- */ + SpinnerModel minNodesSpinnerModel = new LoopSpinnerNumberModel(2,2,4); + add(EDGE_MIN_NODES,new SpinnerWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_min_nodes.title"), + minNodesSpinnerModel, + EDGE_MAX_NODES, + EDGE_LINE_STYLE, + edge.minNodes + )); + + /* --- MAX NODES SELECTION PANEL --- */ + SpinnerModel maxNodesSpinnerModel = new LoopSpinnerNumberModel(2,2,4); + add(EDGE_MAX_NODES, new SpinnerWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_max_nodes.title"), + maxNodesSpinnerModel, + EDGE_YESNO_ARROW_HEAD, + EDGE_MIN_NODES, + edge.maxNodes + ){ + @Override + public void next(){ + int min = Integer.parseInt(edge.minNodes.value); + int max = Integer.parseInt(spinner.getValue().toString()); + if(min > max){ + NarratorFactory.getInstance().speak(resources.getString("dialog.error.min_max")); + }else{ + super.next(); + } + } + + }); + + /* --- SELECT WHETHER THE EDGE MUST HAVE ARROW HEADS OR NOT --- */ + String[] arrowHeadOptions = { + resources.getString("panel.edge_yesno_arrow_head.actions.add"), + resources.getString("panel.edge_yesno_arrow_head.actions.finish") + }; + int[] arrowHeadNexts = {EDGE_ARROW_HEAD,EDGES}; + add(EDGE_YESNO_ARROW_HEAD,new SelectWizardPanel( + dialog.getWizardComponents(), + resources.getString("panel.edge_yesno_arrow_head.title"), + Arrays.asList(arrowHeadOptions), + arrowHeadNexts, + EDGE_MAX_NODES + ){ + @Override + public void next(){ + if(comboBox.getSelectedIndex() == 1){ + Model.Edge newEdge = new Model.Edge(); + Model.copy(edge, newEdge); + model.edges.put(newEdge.id,newEdge); + } + super.next(); + } + }); + + /* --- ARROW HEAD SELECTION PANEL --- */ + add(EDGE_ARROW_HEAD, new ArrowHeadPanel()); + + add(LAST_PANEL, new DummyWizardPanel(dialog.getWizardComponents())); + + SpeechUtilities.changeTabListener((JComponent)dialog.getContentPane(), dialog); + } + + private void add(int index, JWizardPanel panel){ + dialog.getWizardComponents().addWizardPanel(index,panel); + } + + private Diagram createDiagram(Model model){ + /* create the node prototypes */ + Node[] nodes = new Node[model.nodes.size()]; + int i = 0; + for(Model.Node n : model.nodes.values()){ + nodes[i] = createDiagramNode(n); + i++; + } + /* create the edge prototypes */ + Edge[] edges = new Edge[model.edges.size()]; + i = 0; + for(Model.Edge e : model.edges.values()){ + edges[i] = createDiagramEdge(e); + i++; + } + return Diagram.newInstance(model.diagramName.value, nodes, edges, new SimpleShapePrototypePersistenceDelegate()); + } + + private Node createDiagramNode(Model.Node n){ + /* set up the properties */ + LinkedHashMap<String,Set<String>> propertiesTypeDefinition = new LinkedHashMap<String,Set<String>>(); + /* create the property type definition */ + for(Model.Property modelProperty : n.properties.values()){ + Set<String> modifiersTypeDefinition = new LinkedHashSet<String>(); + for(Model.Modifier modifier : modelProperty.modifiers.values()) + modifiersTypeDefinition.add(modifier.type.value); + propertiesTypeDefinition.put(modelProperty.type.value, modifiersTypeDefinition); + } + NodeProperties properties = new NodeProperties(propertiesTypeDefinition); + /* now that properties object is created attach the views on it */ + for(Model.Property modelProperty : n.properties.values()){ + PropertyView propertyView = new PropertyView( + SimpleShapeNode.Position.valueOf(modelProperty.position.value), + modelProperty.shape.value.isEmpty() ? + /* doesn't really matter as position is inside and shape won't be taken into account */ + SimpleShapeNode.ShapeType.Rectangle : + SimpleShapeNode.ShapeType.valueOf(modelProperty.shape.value) + ); + properties.setView(modelProperty.type.value, propertyView); + /* modifier view */ + for(Model.Modifier modelModifier : modelProperty.modifiers.values()){ + boolean bold = false; + boolean italic = false; + boolean underline = false; + String prefix = ""; + String suffix = ""; + for(String value : modelModifier.format.values){ + if(value.equals(resources.getString("modifier.format.bold"))){ + bold = true; + }else if(value.equals(resources.getString("modifier.format.underline"))){ + underline = true; + }else if(value.equals(resources.getString("modifier.format.italic"))){ + italic = true; + }else if(value.equals(resources.getString("modifier.format.prefix"))){ + prefix = modelModifier.affix.values[PREFIX_INDEX]; + }else if(value.equals(resources.getString("modifier.format.suffix"))){ + suffix = modelModifier.affix.values[SUFFIX_INDEX]; + } + } + ModifierView modifierView = new ModifierView(underline,bold,italic,prefix,suffix); + properties.getModifiers(modelProperty.type.value).setView(modelModifier.type.value, modifierView); + } + } + return SimpleShapeNode.getInstance( + SimpleShapeNode.ShapeType.valueOf(n.shape.value), + n.type.value, + properties); + } + + private Edge createDiagramEdge(Model.Edge e){ + /* create the arrow head array out of the string stored in the model */ + ArrowHead[] arrowHeads = new ArrowHead[e.arrowHeads.values.length]; + for(int i=0; i<e.arrowHeads.values.length;i++){ + try { + arrowHeads[i] = ArrowHead.getArrowHeadFromString(e.arrowHeads.values[i]); + } catch (IOException ioe) { + throw new RuntimeException(ioe);// the wizard mustn't allow the user to enter different strings + } + } + return new SimpleShapeEdge( + e.type.value, + LineStyle.valueOf(e.lineStyle.value), + arrowHeads, + e.arrowHeadsDescriptions.values, + Integer.parseInt(e.minNodes.value), + Integer.parseInt(e.maxNodes.value) + ); + } + + private Model createModel(Diagram diagram) { + Model model = new Model(); + if(diagram == null) + return model; + + /* the name isn't copied as the user as to find a new one */ + /* model.diagramName.value = diagram.getName();*/ + + /* nodes */ + for(Node n : diagram.getNodePrototypes()){ + if(!(n instanceof SimpleShapeNode)) + continue; + Model.Node modelNode = createModelNode((SimpleShapeNode)n); + model.nodes.put(modelNode.id,modelNode); + } + /* edges */ + for(Edge e : diagram.getEdgePrototypes()){ + if(!(e instanceof SimpleShapeEdge)) + continue; + Model.Edge modelEdge = createModelEdge((SimpleShapeEdge)e); + model.edges.put(modelEdge.id, modelEdge); + } + return model; + } + + /** + * fills up the model node object with informations from the real diagram node + * @param n + * @return + */ + private Model.Node createModelNode(SimpleShapeNode n){ + Model.Node modelNode = new Model.Node(); + modelNode.type.value = n.getType(); + modelNode.shape.value = n.getShapeType().toString(); + + NodeProperties properties = n.getProperties(); + for(String propertyType : properties.getTypes()){ + Model.Property modelProperty = new Model.Property(); + modelProperty.type.value = propertyType; + /* if the view is not a PropertyView or is null then assign a default value */ + /* it should never happen but it's just to keep it safer and more forward compliant */ + if(! (properties.getView(propertyType) instanceof PropertyView)){ + modelProperty.position.value = SimpleShapeNode.Position.Inside.toString(); + }else{ + PropertyView propertyView = (PropertyView)properties.getView(propertyType); + modelProperty.position.value = propertyView.getPosition().toString(); + modelProperty.shape.value = propertyView.getShapeType().toString(); + } + Modifiers modifiers = properties.getModifiers(propertyType); + for(String modifierType : modifiers.getTypes()){ + Model.Modifier modelModifier = new Model.Modifier(); + modelModifier.type.value = modifierType; + if(modifiers.getView(modifierType) instanceof ModifierView){ + ModifierView modifierView = (ModifierView)modifiers.getView(modifierType); + /* the string array with the modifier values must be created, so the size must be known before */ + int numModifierValues = 0; + if(modifierView.isBold()) + numModifierValues++; + if(modifierView.isItalic()) + numModifierValues++; + if(modifierView.isUnderline()) + numModifierValues++; + if(!modifierView.getPrefix().isEmpty()) + numModifierValues++; + if(!modifierView.getSuffix().isEmpty()) + numModifierValues++; + /* create the string array and fill it up with values */ + modelModifier.format.values = new String[numModifierValues]; + numModifierValues = 0; + if(modifierView.isBold()) + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.bold"); + if(modifierView.isItalic()) + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.italic"); + if(modifierView.isUnderline()) + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.underline"); + if(!modifierView.getPrefix().isEmpty()){ + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.prefix"); + modelModifier.affix.values[PREFIX_INDEX] = modifierView.getPrefix(); + } + + if(!modifierView.getSuffix().isEmpty()){ + modelModifier.format.values[numModifierValues++] = resources.getString("modifier.format.suffix"); + modelModifier.affix.values[SUFFIX_INDEX] = modifierView.getSuffix(); + } + } + modelProperty.modifiers.put(modelModifier.id, modelModifier); + } + modelNode.properties.put(modelProperty.id, modelProperty); + } + return modelNode; + } + + private Model.Edge createModelEdge(SimpleShapeEdge e){ + Model.Edge modelEdge = new Model.Edge(); + modelEdge.type.value = e.getType(); + modelEdge.lineStyle.value = e.getStyle().toString(); + modelEdge.maxNodes.value = Integer.toString(e.getMaxAttachedNodes()); + modelEdge.minNodes.value = Integer.toString(e.getMinAttachedNodes()); + + /* arrow heads and arrowheads descriptions */ + modelEdge.arrowHeadsDescriptions.values = e.getAvailableEndDescriptions(); + modelEdge.arrowHeads.values = new String[e.getHeads().length]; + for(int i =0; i<e.getHeads().length;i++){ + modelEdge.arrowHeads.values[i] = e.getHeads()[i].toString(); + } + + return modelEdge; + } + + private SpeechWizardDialog dialog; + private Diagram diagram; + private ResourceBundle resources; + private Model model; + /* these are the temporary variables where the data are stored during the wizard * + * when a sub task is completed ( node creation, edge creation, property creation)* + * the data stored in the temporary variables are saved in the model */ + private Model.Node node; + private Model.Property property; + private Model.Modifier modifier; + private Model.Edge edge; + + static int HOME = 0; + static int DIAGRAM_NAME = 1; + static int NODES = 2; + static int NODE_TYPE = 3; + static int NODE_DEL = 4; + static int NODE_EDIT = 5; + static int NODE_SHAPE = 6; + static int NODE_YESNO_PROPERTIES = 7; + static int PROPERTIES = 8; + static int PROPERTY_TYPE = 9; + static int PROPERTY_DEL = 10; + static int PROPERTY_EDIT = 11; + static int PROPERTY_POSITION = 12; + static int PROPERTY_SHAPE = 13; + static int PROPERTY_YESNO_MODIFIER = 14; + static int MODIFIERS = 15; + static int MODIFIER_TYPE = 16; + static int MODIFIER_DEL = 17; + static int MODIFIER_EDIT = 18; + static int MODIFIER_FORMAT = 19; + static int EDGES = 20; + static int EDGE_TYPE = 21; + static int EDGE_DEL = 22; + static int EDGE_EDIT = 23; + static int EDGE_LINE_STYLE = 24; + static int EDGE_MIN_NODES = 25; + static int EDGE_MAX_NODES = 26; + static int EDGE_YESNO_ARROW_HEAD = 27; + static int EDGE_ARROW_HEAD = 28; + static int LAST_PANEL = 29; + + private static int PREFIX_INDEX = 0; + private static int SUFFIX_INDEX = 1; + + /* the abstract class from which the panels for Nodes, edges and Modifiers inherit + * It displays the actions (add,edit,delete,finish) on a comboBox. if elementNames is empty + * it means that no element has been created yet and therefore edit and delete actions are disabled + */ + @SuppressWarnings("serial") + private static class ActionChooserPanel extends SpeechWizardPanel{ + ActionChooserPanel(JWizardComponents wizardComponents,Collection<String> elementNames, String title, String[] options, int[] nexts, int previous, Model.Element temporaryElement){ + super(wizardComponents,title,OWN_SWITCH, previous); + this.options = options; + comboBoxModel = new DefaultComboBoxModel(); + comboBoxModel.addElement(options[0]); + comboBoxModel.addElement(options[3]); + comboBox = new LoopComboBox(comboBoxModel); + comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + layoutComponents(comboBox); + this.elementNames = elementNames; + this.temporaryElement = temporaryElement; + this.nexts = nexts; + } + + @Override + public void update(){ + if(elementNames.isEmpty() && comboBoxModel.getSize() == 4){ + comboBoxModel.removeElement(options[1]); + comboBoxModel.removeElement(options[2]); + }else if(!elementNames.isEmpty() && comboBoxModel.getSize() == 2){ + comboBoxModel.insertElementAt(options[1],1); + comboBoxModel.insertElementAt(options[2],2); + } + super.update(); + } + + @Override + public void next(){ + /* if the selection was add element, then we clear the temporary holder */ + if(comboBox.getSelectedIndex() == 0) + temporaryElement.clear(); + /* jump to the selected next step, works both when it's only add/finish and when it's add/delete/edit/finish */ + for(int i=0; i<options.length; i++) + if(comboBox.getSelectedItem().equals(options[i])){ + switchPanel(nexts[i]); + return; + } + } + + JComboBox comboBox; + Collection<String> elementNames; + DefaultComboBoxModel comboBoxModel; + String[] options; + Model.Element temporaryElement; + int[] nexts; + } + + @SuppressWarnings("serial") + private static class DeletePanel extends SpeechWizardPanel { + DeletePanel(JWizardComponents wizardComponents,String title, int next, int previous, ModelMap<? extends Model.Element> elements){ + super(wizardComponents,title,next, previous); + this.elements = elements; + comboBox = new LoopComboBox(); + comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); + layoutComponents(comboBox); + } + + @Override + public void update(){ + String[] options = new String[elements.values().size()]; + options = elements.getNames().toArray(options); + comboBox.setModel(new DefaultComboBoxModel(options)); + super.update(); + } + + /** + * the default behaviour is to delete the selected element + */ + @Override + public void next(){ + Model.Element elementToDelete = null; + for(Model.Element element : elements.values()){ + if(element.type.value.equals(comboBox.getSelectedItem())){ + elementToDelete = element; + break; + } + } + Object o = elements.remove(elementToDelete.id); + assert(o != null); + super.next(); + } + + JComboBox comboBox; + ModelMap<? extends Model.Element> elements; + } + + @SuppressWarnings("serial") + private static class EditPanel extends DeletePanel { + EditPanel(JWizardComponents wizardComponents, + String title, + int next, + int previous, + ModelMap<? extends Model.Element> elements, + Model.Element temporaryHolder){ + super(wizardComponents, title,next, previous,elements); + this.temporaryHolder = temporaryHolder; + this.next = next; + } + + @Override + public void next(){ + Model.Element selected = null; + for(Model.Element e : elements.values()){ + if(e.type.value.equals(comboBox.getSelectedItem())){ + selected = e; + break; + } + } + + Model.copy(selected, temporaryHolder); + switchPanel(next); + } + + int next; + Model.Element temporaryHolder; + } + + @SuppressWarnings("serial") + private class FormatWizardPanel extends SpeechWizardPanel{ + FormatWizardPanel(){ + super(dialog.getWizardComponents(),resources.getString("panel.modifier_format.title"),MODIFIERS,MODIFIER_TYPE); + String values[] = { + resources.getString("modifier.format.bold"), + resources.getString("modifier.format.underline"), + resources.getString("modifier.format.italic"), + resources.getString("modifier.format.prefix"), + resources.getString("modifier.format.suffix"), + }; + + checkBoxes = new JCheckBox[values.length]; + checkBoxPanel = new JPanel(new GridLayout(0, 1)); + for(int i=0; i<values.length;i++){ + String value = values[i]; + checkBoxes[i] = new JCheckBox(value); + checkBoxes[i].getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + checkBoxes[i].getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + checkBoxes[i].addItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + /* prefix and suffix check boxes must have a JText area for the user to enter the String */ + if(i == 3 || i == 4){ + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT,0,0)); + panel.add(checkBoxes[i]); + if(i == 3){ + prefixTextField = new JTextField(); + prefixTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + prefixTextField.getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + prefixTextField.getAccessibleContext().setAccessibleName(checkBoxes[i].getText()); + prefixTextField.setColumns(5); + prefixTextField.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + panel.add(prefixTextField); + }else{ + suffixTextField = new JTextField(); + suffixTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + suffixTextField.getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + suffixTextField.getAccessibleContext().setAccessibleName(checkBoxes[i].getText()); + suffixTextField.setColumns(5); + suffixTextField.addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + panel.add(suffixTextField); + } + checkBoxPanel.add(panel); + }else{ + checkBoxPanel.add(checkBoxes[i]); + } + } + JScrollPane scrollPane = new JScrollPane(checkBoxPanel); + scrollPane.setFocusable(false); + layoutComponents(scrollPane); + dialog.getWizardComponents().getFinishButton().setEnabled(true); + } + + /* store the checks into the StrArrayRecord, it doesn't call super.next thus */ + /* sub classes have to implement call the switch panel on their own */ + public void next(){ + int numCheckedBoxes = 0; + for(JCheckBox check : checkBoxes){ + if(check.isSelected()) + numCheckedBoxes++; + } + String[] result = new String[numCheckedBoxes]; + numCheckedBoxes = 0; + for(int i=0; i<checkBoxes.length;i++){ + /* store the text value of the check boxes, if it's the prefix or suffix */ + /* append the text entered by the user in the text areas */ + if(checkBoxes[i].isSelected()){ + String text = checkBoxes[i].getText(); + if(i == 3) + modifier.affix.values[PREFIX_INDEX] = prefixTextField.getText(); + else if(i == 4) + modifier.affix.values[SUFFIX_INDEX] = suffixTextField.getText(); + result[numCheckedBoxes++] = text; + } + } + modifier.format.values = result; + Model.Modifier newModifier = new Model.Modifier(); + Model.copy(modifier,newModifier); + property.modifiers.put(newModifier.id,newModifier); + super.next(); + } + + @Override + public void update(){ + /* set the check boxes and text field according to the modifier.format. so if we are editing an existing * + * modifier we find the old values, else if it's a new modifier we find everything blank */ + if(modifier.format != null){ + prefixTextField.setText(""); + suffixTextField.setText(""); + for(JCheckBox check : checkBoxes){ + /* temporarily remove the speech Item listener in order to avoid bla bla bla not triggered by user */ + check.removeItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + check.setSelected(false); + for(String checkedValue : modifier.format.values){ + if(checkedValue.equals(check.getText())){//for bold,italic,underline + check.setSelected(true); + if(checkedValue.equals(resources.getString("modifier.format.prefix"))){ + prefixTextField.setText(modifier.affix.values[PREFIX_INDEX]); + }else if(checkedValue.equals(resources.getString("modifier.format.suffix"))){ + suffixTextField.setText(modifier.affix.values[SUFFIX_INDEX]); + } + break; + } + } + check.addItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + } + } + super.update(); + } + + @Override + protected Component assignFocus(){ + /* focus on the first item */ + checkBoxes[0].requestFocus(); + return checkBoxes[0]; + } + + JTextField prefixTextField; + JTextField suffixTextField; + JPanel checkBoxPanel; + JCheckBox checkBoxes[]; + } + + @SuppressWarnings("serial") + private class ArrowHeadPanel extends SpeechWizardPanel { + ArrowHeadPanel(){ + super(dialog.getWizardComponents(),resources.getString("panel.edge_arrow_head.title"),EDGES,EDGE_YESNO_ARROW_HEAD); + JPanel panel = new JPanel(new GridBagLayout()); + final JScrollPane scrollPane = new JScrollPane(panel); + scrollPane.setFocusable(false); + GridBagUtilities gridBagUtils = new GridBagUtilities(); + int numArrowHeads = ArrowHead.values().length; + arrowsCheckBoxes = new JCheckBox[numArrowHeads]; + arrowsTextDescriptions = new JTextField[numArrowHeads]; + for(int i=0; i<numArrowHeads; i++){ + /* set up the key bindings for all the check boxes and text fields */ + /* by pressing enter the wizard switches the next panel */ + arrowsCheckBoxes[i] = new JCheckBox(ArrowHead.values()[i].toString()); + arrowsCheckBoxes[i].getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + arrowsCheckBoxes[i].getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + arrowsTextDescriptions[i] = new JTextField(); + arrowsTextDescriptions[i].getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "enter"); + arrowsTextDescriptions[i].getActionMap().put("enter", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + getWizardComponents().getNextButton().doClick(); + } + }); + /* add the speech to the check boxes */ + arrowsCheckBoxes[i].addItemListener(SpeechUtilities.getCheckBoxSpeechItemListener()); + + arrowsTextDescriptions[i].setPreferredSize(new Dimension(TEXTFIELD_SIZE, arrowsTextDescriptions[i].getPreferredSize().height)); + arrowsTextDescriptions[i].getAccessibleContext().setAccessibleName(arrowsCheckBoxes[i].getText()); + arrowsTextDescriptions[i].addKeyListener(SpeechUtilities.getSpeechKeyListener(true)); + panel.add(arrowsCheckBoxes[i], gridBagUtils.label()); + panel.add(arrowsTextDescriptions[i],gridBagUtils.field()); + } + layoutComponents(scrollPane); + } + + @Override + public void update(){ + /* restore the values (checkbox + text) currently in edge.arrowHeads into the panel components */ + if(edge.arrowHeads != null){ + for(int i=0; i<arrowsCheckBoxes.length;i++){ + arrowsCheckBoxes[i].setSelected(false); + arrowsTextDescriptions[i].setText(""); + for(int j=0; j< edge.arrowHeads.values.length; j++){ + if(arrowsCheckBoxes[i].getText().equals(edge.arrowHeads.values[j])){ + arrowsCheckBoxes[i].setSelected(true); + arrowsTextDescriptions[i].setText(edge.arrowHeadsDescriptions.values[j]); + break; + } + } + } + } + super.update(); + } + @Override + public void next(){ + /* check that the user has entered a text for all of the selected check boxes */ + int numChecked = 0;//this is to keep count of the checked boxes, used after the check + for(int i=0; i<arrowsCheckBoxes.length;i++){ + JCheckBox checkBox = arrowsCheckBoxes[i]; + if(checkBox.isSelected()){ + numChecked++; + /* there cannot be a checked check box without the related textField filled in */ + if(arrowsTextDescriptions[i].getText().trim().isEmpty()){ + NarratorFactory.getInstance().speak( + MessageFormat.format( + resources.getString("dialog.error.empty_desc"), + checkBox.getText()) + ); + return; + } + } + } + /* copy the label of the checked boxes and the text of the JTextField into the edge fields */ + edge.arrowHeads.values = new String[numChecked]; + edge.arrowHeadsDescriptions.values = new String[numChecked]; + numChecked = 0; + for(int i=0; i<arrowsCheckBoxes.length;i++){ + if(arrowsCheckBoxes[i].isSelected()){ + edge.arrowHeads.values[numChecked] = arrowsCheckBoxes[i].getText(); + edge.arrowHeadsDescriptions.values[numChecked] = arrowsTextDescriptions[i].getText().trim(); + numChecked++; + } + } + /* put the edge (copy of) into the model */ + Model.Edge newEdge = new Model.Edge(); + Model.copy(edge, newEdge); + model.edges.put(newEdge.id,newEdge); + super.next(); + } + + @Override + protected Component assignFocus(){ + /* focus on the first item */ + arrowsCheckBoxes[0].requestFocus(); + return arrowsCheckBoxes[0]; + } + + JCheckBox arrowsCheckBoxes[]; + JTextField arrowsTextDescriptions[]; + final int TEXTFIELD_SIZE = 100; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/AudioResourcesService.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,74 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Set; + +/** + * This class holds the stream of audio files associated to each + * sound event type. The audio file is played out each time the event, associated to + * it, happens. + */ + +public class AudioResourcesService { + /** + * @param type A sound event type + * @return the file name associated to a sound event type + */ + public static InputStream getAudiofile(SoundEvent type){ + if(audioFileNames == null) + audioFileNames = new AudioResourcesService(); + return audioFileNames.nameMap.get(type); + } + + public static Set<SoundEvent> eventTypes(){ + if(audioFileNames == null) + audioFileNames = new AudioResourcesService(); + return audioFileNames.nameMap.keySet(); + } + + private AudioResourcesService(){ + Class<AudioResourcesService> c = AudioResourcesService.class; + nameMap = new LinkedHashMap<SoundEvent, InputStream>(); + nameMap.put(SoundEvent.TREE_NODE_COLLAPSE, c.getResourceAsStream("audio/collapse.mp3")); + nameMap.put(SoundEvent.TREE_NODE_EXPAND, c.getResourceAsStream("audio/expand.mp3")); + nameMap.put(SoundEvent.LIST_BOTTOM_REACHED, c.getResourceAsStream("audio/endoflist.mp3")); + nameMap.put(SoundEvent.LIST_TOP_REACHED, c.getResourceAsStream("audio/endoflist.mp3")); + nameMap.put(SoundEvent.JUMP,c.getResourceAsStream("audio/jump.mp3")); + nameMap.put(SoundEvent.ERROR,c.getResourceAsStream("audio/error.mp3")); + nameMap.put(SoundEvent.OK,c.getResourceAsStream("audio/Ok.mp3")); + nameMap.put(SoundEvent.CANCEL,c.getResourceAsStream("audio/cancel.mp3")); + nameMap.put(SoundEvent.MESSAGE_OK,c.getResourceAsStream("audio/cancel.mp3")); + nameMap.put(SoundEvent.EMPTY,c.getResourceAsStream("audio/cancel.mp3")); + nameMap.put(SoundEvent.EDITING, c.getResourceAsStream("audio/editingMode.mp3")); + nameMap.put(SoundEvent.MAGNET_ON, c.getResourceAsStream("audio/magnetON.mp3")); + nameMap.put(SoundEvent.MAGNET_OFF, c.getResourceAsStream("audio/magnetOFF.mp3")); + nameMap.put(SoundEvent.HOOK_ON,c.getResourceAsStream("audio/hookON.mp3")); + nameMap.put(SoundEvent.HOOK_OFF,c.getResourceAsStream("audio/hookOFF.mp3")); + nameMap.put(SoundEvent.DRAG, c.getResourceAsStream("audio/drag.mp3")); + } + + private LinkedHashMap<SoundEvent, InputStream> nameMap; + private static AudioResourcesService audioFileNames; + public static String FOLDER = "audio/"; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/BeadsSound.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,182 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +import java.io.InputStream; +import java.util.EnumMap; +import java.util.Map; + +import net.beadsproject.beads.core.AudioContext; +import net.beadsproject.beads.core.Bead; +import net.beadsproject.beads.core.UGen; +import net.beadsproject.beads.data.Sample; +import net.beadsproject.beads.data.SampleManager; +import net.beadsproject.beads.ugens.Gain; +import net.beadsproject.beads.ugens.SamplePlayer; +import net.beadsproject.beads.ugens.SamplePlayer.LoopType; + +/** + * The Sound interface implementation using the Beads library. + * For more info about the library see http://www.beadsproject.net/. + */ +class BeadsSound implements Sound { + + public BeadsSound(){ + ac = new AudioContext(); + playerListeners = new EnumMap<SoundEvent,PlayerListener>(SoundEvent.class); + loopPlayers = new EnumMap<SoundEvent,UGen>(SoundEvent.class); + + /* pre load all the sample to avoid future overhead */ + for(SoundEvent key : AudioResourcesService.eventTypes()){ + SampleManager.sample(AudioResourcesService.getAudiofile(key)); + } + ac.start(); + } + + + public void play(InputStream sound, final PlayerListener playerListener) { + if(mute) + return; + SamplePlayer player; + Sample sample = null; + if(sound != null) + sample = SampleManager.sample(sound); + if(sample == null){ + /* we got problems retrieving the sample to play + * call the playerListener method and return */ + if(playerListener != null) + playerListener.playEnded(); + return; + } + player = new SamplePlayer(ac,sample); + player.setKillOnEnd(true); + final Gain g = new Gain(ac,1,MASTER_VOLUME); + g.addInput(player); + + Bead killBill; + if(playerListener != null){ + killBill = new Bead(){ + @Override + protected void messageReceived(Bead message){ + playerListener.playEnded(); + g.kill(); + playingBead = null; + } + }; + }else{ + killBill = new Bead(){ + @Override + protected void messageReceived(Bead message){ + g.kill(); + playingBead = null; + } + }; + } + + player.setKillListener(killBill); + playingBead = g; + ac.out.addInput(g); + } + + public void play(InputStream sound){ + play(sound, null); + } + + @Override + public void play(final SoundEvent evt ){ + if(evt == null){ + InputStream s = null; + play(s); + }else + play(evt,playerListeners.get(evt)); + } + + public void play(SoundEvent evt, PlayerListener playerListener){ + if(evt == null){ + InputStream s = null; + play(s,playerListener); + }else + play(AudioResourcesService.getAudiofile(evt),playerListener); + } + + public void stop(){ + if(mute) + return; + if(playingBead != null){ + playingBead.setKillListener(null); + playingBead.kill(); + } + } + + public void loadSound(InputStream sound){ + SampleManager.sample(sound); + } + + @Override + public void startLoop(SoundEvent action) { + if(mute) + return; + Sample sample = null; + if(action != null){ + InputStream samplePath = AudioResourcesService.getAudiofile(action); + if(samplePath != null) + sample = SampleManager.sample(samplePath); + } + if(sample == null) + return; + SamplePlayer player = new SamplePlayer(ac,sample); + player.setLoopType(LoopType.LOOP_FORWARDS); + Gain g = new Gain(ac,1,MASTER_VOLUME); + g.addInput(player); + ac.out.addInput(g); + loopPlayers.put(action, g); + } + + @Override + public void stopLoop(SoundEvent action) { + UGen g = loopPlayers.get(action); + if(g != null){ + g.kill(); + loopPlayers.remove(action); + } + } + + @Override + public void setDefaultPlayerListener(PlayerListener listener, SoundEvent type){ + playerListeners.put(type, listener); + } + + @Override + public void setMuted(boolean mute){ + this.mute = mute; + } + + @Override + public void dispose(){ + ac.stop(); + } + + private AudioContext ac; + private Bead playingBead; + private Map<SoundEvent,PlayerListener> playerListeners; + private Map<SoundEvent,UGen> loopPlayers; + private static final float MASTER_VOLUME = 0.35f; + private boolean mute; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/PlayerListener.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,33 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +/** + * {@code PlayerListeners} can be registered to an object implementing the + * {@code Sound} interface. Each time a sound is played registered listeners + * will be triggered just after the sound is played. + * + */ +public interface PlayerListener { + /** + * Called when the sound is played + */ + void playEnded(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/Sound.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,57 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +import java.io.InputStream; + +/** + * An object implementing the {@code Sound} interface can be used to play sound + * samples either just once or in a continuous loop. The client class needs to provide + * a reference to an {@code InputStream} to the sample source. + * Furthermore, prebuilt sounds + * associated to the events defined by the {@code SoundEvent} enumeration can be played. + * + */ +public interface Sound { + + public void play(SoundEvent evt); + + public void play(SoundEvent evt, PlayerListener listener); + + public void play(InputStream sound); + + public void play(InputStream sound, PlayerListener listener); + + public void stop(); + + public void setMuted(boolean mute); + + public void loadSound(InputStream sound); + + public void startLoop(SoundEvent evt); + + public void stopLoop(SoundEvent evt); + + public void setDefaultPlayerListener(PlayerListener listener, + SoundEvent type); + + public void dispose(); + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/SoundEvent.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,45 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +/** + * The events for which the {@code Sound} library provides predefined sounds. + * The sound files sources for each SoundEvent can be retrieved through the {@code AudioResourcesService} + * class. + * + */ +public enum SoundEvent { + ERROR, + TREE_NODE_COLLAPSE, + TREE_NODE_EXPAND, + LIST_TOP_REACHED, + LIST_BOTTOM_REACHED, + JUMP, + OK, + CANCEL, + EMPTY, + EDITING, + MAGNET_ON, + MAGNET_OFF, + HOOK_ON, + HOOK_OFF, + DRAG, + MESSAGE_OK +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/sound/SoundFactory.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,40 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.sound; + +/** + * The factory class to create {@code Sound} instances. + * + */ +public abstract class SoundFactory { + + public static Sound createInstance(){ + sound = new BeadsSound(); + return sound; + } + + public static Sound getInstance(){ + if(sound == null) + throw new IllegalStateException("createInstance() must be called before any getInstance() call"); + return sound; + } + + private static Sound sound; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/BeadsAudioPlayer.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,217 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.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; + +/** + * An implementation of {@code AudioPlayer} using the {@code Beads} + * library. + */ +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; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/DummyNarrator.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,61 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +/** +* A dummy implementation of the Narrator interface. All its methods are empty, +* so every call will have no effect whatsoever. +*/ +class DummyNarrator implements Narrator { + + @Override + public void init() throws NarratorException {} + + @Override + public void setMuted(boolean muted, 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 + public void setRate(int rate) { } + + @Override + public int getRate() {return 0;} + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,56 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +/** + * + * The {@code Narrator} interface provides high level methods to make use of text to speech synthesis. + * + */ +public interface Narrator { + + public void init() throws NarratorException; + + public void setMuted(boolean muted, int voice); + + public boolean isMuted(int voice); + + public void setRate(int rate); + + public int getRate(); + + 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; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/Narrator.properties Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,31 @@ +component.button=button +component.text_field=text field +component.text_area=text Area +component.combo_box=selected +component.spinner=number box +component.chech=checked +component.uncheck=unchecked + +char.back_space=back space +char.at=at +char.new_line=new line +char.dot=dot +char.comma=comma +char.colon=colon +char.semi_colon=semi colon +char.lower_than=lower than +char.greater_than=greater than +char.delete=delete +char.sharp=number sign +char.tilde=tilde +char.slash=slash +char.plus=plus +char.dash=dash +char.underscore=underscore +char.space=space +char.asterisk=asterisk +char.dollar=dollar + +error.no_speech=Could not create the speech synthesizer + +ccmi_spell=dot c c m i
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/NarratorException.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,38 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +import java.util.ResourceBundle; + +/** + * A {@code NarratorException} is thrown when a text to speech synthesizer cannot be instantiated + * for any reason. + * + */ +@SuppressWarnings("serial") +public class NarratorException extends Exception { + public NarratorException(String msg){ + super(msg); + } + + public NarratorException(){ + super(ResourceBundle.getBundle(Narrator.class.getName()).getString("error.no_speech")); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/NarratorFactory.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,56 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +import java.util.logging.Logger; + +import uk.ac.qmul.eecs.ccmi.utils.OsDetector; + +/** + * The factory class to create {@code Narrator} instances. + * + */ +public abstract class NarratorFactory { + + public static Narrator createInstance(){ + if(singleNarrator == null){ + if(OsDetector.isWindows()){ + try{ + singleNarrator = new NativeNarrator(); + singleNarrator.init(); + }catch(NarratorException ne){ + singleNarrator = new DummyNarrator(); + Logger.getLogger("general").warning("Could not enable text to speech synthesis"); + } + }else{ + singleNarrator = new DummyNarrator(); + } + } + return singleNarrator; + } + + public static Narrator getInstance(){ + if(singleNarrator == null) + throw new IllegalStateException("createInstance() must be called before any getInstance() call"); + return singleNarrator; + } + + private static Narrator singleNarrator; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/NativeNarrator.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,235 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +import java.net.URL; +import java.util.ResourceBundle; +import java.util.concurrent.LinkedBlockingQueue; + +import uk.ac.qmul.eecs.ccmi.utils.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()){ + String res = OsDetector.has64BitJVM() ? "WinNarrator64.dll" : "WinNarrator.dll" ; + URL url = NativeNarrator.class.getResource(res); + if(url != null){ + ResourceFileWriter fileWriter = new ResourceFileWriter(url); + fileWriter.writeOnDisk( + PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")), + OsDetector.has64BitJVM() ? "CCmIWinNarrator64.dll" : "CCmIWinNarrator.dll"); + String path = fileWriter.getFilePath(); + if(path != null) + try{ + System.load( path ); + nativeLibraryNotFound = false; + }catch(UnsatisfiedLinkError e){ + e.printStackTrace(); + /* do nothing: nativeLibraryNotFound won't be set to false */ + /* which will trigger a NarratorException */ + } + } + } + } + + 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 + public void init() throws NarratorException { + if(nativeLibraryNotFound) + throw new NarratorException(); + + firstVoiceMuted = false; + secondVoiceMuted = false; + queue = new LinkedBlockingQueue<QueueEntry>(); + executor = new Executor(); + boolean success = _init(); + if(!success) + throw new NarratorException(); + rate = Integer.parseInt(PreferencesService.getInstance().get("speech_rate", DEFAULT_RATE_VALUE)); + _setRate(rate); + executor.start(); + } + + @Override + public void setMuted(boolean muted, 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 + public void setRate(int rate){ + if(rate < MIN_RATE || rate > MAX_RATE) + throw new IllegalArgumentException("Rate value must be between 0 and 20"); + _setRate(rate); + this.rate = rate; + PreferencesService.getInstance().put("speech_rate", Integer.toString(rate)); + } + + @Override + public int getRate(){ + return rate; + } + + @Override + public void shutUp() { + _shutUp(); + } + + @Override + public void speak(String text, 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,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) { + speakWholeText(text, Narrator.FIRST_VOICE); + } + + @Override + public void dispose() { + executor.mustSayGoodbye = true; + executor.interrupt(); + } + + /* native routines used by the Executor thread */ + private native boolean _init(); + + private native void _dispose(); + + private native void _speak(String text); + + private native void _speakWholeText(String text); + + private native void _setRate(int rate); + + private native void _shutUp(); + + private volatile boolean firstVoiceMuted; + private volatile boolean secondVoiceMuted; + private int rate; + private LinkedBlockingQueue<QueueEntry> queue; + private Executor executor; + private ResourceBundle resources; + private Voice secondaryVoice; + private static String DEFAULT_RATE_VALUE = "13"; + private static final String VOICE_NAME = "kevin16"; + private static boolean nativeLibraryNotFound; + + private class Executor extends Thread{ + private Executor(){ + super("Narrator Thread"); + mustSayGoodbye = false; + } + + @Override + public void run(){ + QueueEntry entry; + while(!mustSayGoodbye){ + try { + entry = queue.take(); + } catch (InterruptedException e) { + continue; /* start over the while cycle */ + } + if(!entry.speakToEnd && queue.peek() != null) + continue;/* the user submitted another text to be spoken out and this can be overwritten */ + if(entry.speakToEnd && entry.voice == Narrator.FIRST_VOICE){ + _speakWholeText(entry.text); + }else if(entry.voice == Narrator.FIRST_VOICE){ + _speak(entry.text); + }else if(secondaryVoice != null){ + secondaryVoice.speak(entry.text); + } + } + if(secondaryVoice != null) + secondaryVoice.deallocate(); + _dispose(); + } + + private volatile boolean mustSayGoodbye; + } + + private static class QueueEntry { + QueueEntry(String text, boolean speakToEnd, int voice){ + this.text = text; + this.speakToEnd = speakToEnd; + this.voice = voice; + } + String text; + boolean speakToEnd; + int voice; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/SpeechUtilities.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,401 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.speech; + +import java.awt.AWTKeyStroke; +import java.awt.Component; +import java.awt.Container; +import java.awt.FocusTraversalPolicy; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.ResourceBundle; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JSpinner; +import javax.swing.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. + * + */ +public abstract class SpeechUtilities { + /* this class is of static use only */ + private SpeechUtilities(){} + + public static String getComponentSpeech(Component c){ + StringBuilder b = new StringBuilder(); + if(c.getAccessibleContext().getAccessibleName() != null) + b.append(c.getAccessibleContext().getAccessibleName()); + if(c instanceof JButton) + b.append(' ').append(resources.getString("component.button")); + else if(c instanceof JTextField){ + b.append(' ').append(resources.getString("component.text_field")); + b.append(((JTextField)c).getText()); + }else if(c instanceof JTextArea){ + b.append(' ').append(resources.getString("component.text_area")); + b.append(((JTextArea)c).getText()); + }else if(c instanceof JComboBox){ + b.append(((JComboBox)c).getSelectedItem().toString()); + b.append(' ').append(resources.getString("component.combo_box")); + }else if(c instanceof JCheckBox){ + b.append(' ').append(((JCheckBox)c).isSelected() ? resources.getString("component.chech") : resources.getString("component.uncheck")); + }else if(c instanceof JSpinner){ + b.append(' ').append(resources.getString("component.spinner")); + b.append(((JSpinner)c).getValue()); + }else 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(); + } + + @SuppressWarnings("serial") + public static void changeTabListener(JComponent component, final Container container){ + /* remove the default tab traversal key from all the containers */ + disableTraversalKey(component); + /* get the look and feel default keys for moving the focus on (usually = TAB) */ + for(AWTKeyStroke keyStroke : KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)) + component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(keyStroke.getKeyCode(),keyStroke.getModifiers()),"tab"); + + /* add action to the moving focus keys: reproduce focus system and add speech to it */ + component.getActionMap().put("tab", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FocusTraversalPolicy policy = KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalPolicy(); + Component next = policy.getComponentAfter(container, KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); + if(next == null) + return; + next.requestFocusInWindow(); + NarratorFactory.getInstance().speak(SpeechUtilities.getComponentSpeech(next)); + InteractionLog.log("TABBED PANE","change focus ",next.getAccessibleContext().getAccessibleName()); + } + }); + + for(AWTKeyStroke keyStroke : KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)) + component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(keyStroke.getKeyCode(),keyStroke.getModifiers()),"back_tab"); + + component.getActionMap().put("back_tab", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + FocusTraversalPolicy policy = KeyboardFocusManager.getCurrentKeyboardFocusManager().getDefaultFocusTraversalPolicy(); + Component previous = policy.getComponentBefore(container, KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); + if(previous == null) + return; + 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){ + for(final Component c : container.getComponents()){ + if(c instanceof Container){ + c.setFocusTraversalKeysEnabled(false); + disableTraversalKey((Container)c); + } + } + } + + 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 whether this key listener is for a component + * that will be editable + * @return a key listener that utters the letters when typed + */ + public static KeyListener getSpeechKeyListener(boolean editableComponent){ + return getSpeechKeyListener(editableComponent,false); + } + + public static ItemListener getSpeechComboBoxItemListener(){ + return comboBoxItemListener; + } + + public static ItemListener getCheckBoxSpeechItemListener(){ + return checkBoxItemListener; + } + + public static Action getShutUpAction(){ + return shutUpAction; + } + + /* + * this class manages the speech feedback when moving around a text component + * with the up, down, left and right arrows + */ + private static class SpeechKeyListener extends KeyAdapter{ + boolean isTab; + boolean isBeginning; + boolean isFirstLine; + boolean isLastLine; + boolean editableComponent; + int voice; + + SpeechKeyListener(boolean editablecomponent, boolean useSecondVoice){ + this.editableComponent = editablecomponent; + voice = useSecondVoice ? Narrator.SECOND_VOICE : Narrator.FIRST_VOICE; + } + + @Override + public void keyTyped(KeyEvent evt){ + /* this will manage digit or letter characters */ + if(!isTab && !evt.isControlDown() && editableComponent){ + if(Character.isLetterOrDigit(evt.getKeyChar())){ + NarratorFactory.getInstance().speak(String.valueOf(evt.getKeyChar()),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"),voice); + break; + case ' ' : + NarratorFactory.getInstance().speak(resources.getString("char.space"),voice); + break; + case '@' : + 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"),voice); + break; + case ',' : + NarratorFactory.getInstance().speak(resources.getString("char.comma"),voice); + break; + case ';' : + NarratorFactory.getInstance().speak(resources.getString("char.semi_colon"),voice); + break; + case ':' : + NarratorFactory.getInstance().speak(resources.getString("char.colon"),voice); + break; + case '<' : + NarratorFactory.getInstance().speak(resources.getString("char.lower_than"),voice); + break; + case '>' : + NarratorFactory.getInstance().speak(resources.getString("char.greater_than"),voice); + break; + case '#' : + NarratorFactory.getInstance().speak(resources.getString("char.sharp"),voice); + break; + case '~' : + NarratorFactory.getInstance().speak(resources.getString("char.tilde"),voice); + break; + case '+' : + NarratorFactory.getInstance().speak(resources.getString("char.plus"),voice); + break; + case '-' : + NarratorFactory.getInstance().speak(resources.getString("char.dash"),voice); + break; + case '_' : + NarratorFactory.getInstance().speak(resources.getString("char.underscore"),voice); + break; + case '/' : + NarratorFactory.getInstance().speak(resources.getString("char.slash"),voice); + break; + } + } + } + isTab = false; + } + + /* manages all the non digit or letter characters */ + @Override + public void keyPressed(KeyEvent e){ + int caretPos = ((JTextComponent)e.getSource()).getCaretPosition(); + String text = ((JTextComponent)e.getSource()).getText(); + + if (e.getKeyCode() == KeyEvent.VK_TAB){ + isTab = true; + } + if(caretPos == 0) + isBeginning = true; + else + isBeginning = false; + + isFirstLine = true; + for(int i=0; i<caretPos;i++){ + if(text.charAt(i) == '\n'){ + isFirstLine = false; + break; + } + } + + if(text.indexOf('\n', caretPos) == -1) + isLastLine = true; + else + isLastLine = false; + } + + @Override + public void keyReleased(KeyEvent evt){ + JTextComponent textComponent = (JTextComponent)evt.getSource(); + String text; + int begin,end,caretPos; + + switch(evt.getKeyCode()){ + case KeyEvent.VK_BACK_SPACE: + NarratorFactory.getInstance().speak(resources.getString("char.back_space"),voice); + break; + case KeyEvent.VK_DELETE : + NarratorFactory.getInstance().speak(resources.getString("char.delete"),voice); + break; + case KeyEvent.VK_LEFT : + case KeyEvent.VK_RIGHT : + try { + if(evt.getKeyCode() == KeyEvent.VK_LEFT){ //left + if(textComponent.getCaretPosition() == 0 && isBeginning){ + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + }else{ // right + if(textComponent.getCaretPosition() == textComponent.getText().length()){ + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + } + NarratorFactory.getInstance().speak(textComponent.getText(textComponent.getCaretPosition(),1),voice); + } catch (BadLocationException e1) { + e1.printStackTrace(); + } + break; + case KeyEvent.VK_UP : + /* when moving up and down, the line we land on is spoken out (the whole line). If the border + * (top/bottom most line) is reached then the error sound is played. on a JTextField this + * is the default behaviour with up and down keys as we only have one line + */ + + if(isFirstLine){//we're on the first line and cannot go any upper + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + + text = textComponent.getText(); + caretPos = textComponent.getCaretPosition(); + + /* look for the beginning of the row the cursor is */ + begin = 0; + for(int i=0; i<caretPos;i++){ + if(text.charAt(i) == '\n') + begin = i+1; + } + + /* now the end */ + end = text.indexOf('\n', caretPos); + + if(end == begin)//in case it's an empty line + end++; + NarratorFactory.getInstance().speak(text.substring(begin, end),voice); + break; + case KeyEvent.VK_DOWN : + if(isLastLine){ //no new line we either have one line only or sit on the last one + SoundFactory.getInstance().play(SoundEvent.ERROR); + return; + } + + text = textComponent.getText(); + caretPos = textComponent.getCaretPosition(); + + begin = 0; + for(int i=0;i<caretPos;i++){ + if(text.charAt(i) == '\n') + begin = i+1; + } + begin = Math.min(begin, text.length()-1); + + end = text.indexOf('\n', begin); + if(end == -1) // the line we're looking for is the last one + end = text.length()-1; + + if(end == begin) // in case it's an empty line + end++; + NarratorFactory.getInstance().speak(text.substring(begin, end),voice); + break; + } + } + } + + private static final ResourceBundle resources = ResourceBundle.getBundle(Narrator.class.getName()); + private static final SpeechKeyListener speechKeyListener = new SpeechKeyListener(true,false); + private static final ItemListener comboBoxItemListener = new ItemListener(){ + @Override + public void itemStateChanged(ItemEvent evt) { + if(evt.getStateChange() == ItemEvent.SELECTED) + NarratorFactory.getInstance().speak(evt.getItem().toString()); + } + }; + + private static final ItemListener checkBoxItemListener = new ItemListener(){ + @Override + public void itemStateChanged(ItemEvent evt) { + NarratorFactory.getInstance().speak(getComponentSpeech(((JCheckBox)evt.getItemSelectable()))); + } + }; + + @SuppressWarnings("serial") + private static final Action shutUpAction = new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent e) { + NarratorFactory.getInstance().shutUp(); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/speech/TreeSonifier.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,163 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package uk.ac.qmul.eecs.ccmi.speech; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; + +import uk.ac.qmul.eecs.ccmi.sound.PlayerListener; +import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; +import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; + +/** + * + */ +@SuppressWarnings("serial") +public class TreeSonifier { + + public void sonify(final JTree tree){ + /* select the root node as selected */ + tree.setSelectionRow(0); + /* Overwrite keystrokes up,down,left,right arrows and space, shift, ctrl */ + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0),"down"); + tree.getActionMap().put("down", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); + /* look if we've got a sibling node after (we are not at the bottom) */ + DefaultMutableTreeNode nextTreeNode = treeNode.getNextSibling(); + SoundEvent loop = null; + if(nextTreeNode == null){ + DefaultMutableTreeNode parent = (DefaultMutableTreeNode)treeNode.getParent(); + if(parent == null) /* root node, just stay there */ + nextTreeNode = treeNode; + else /* loop = go to first child of own parent */ + nextTreeNode = (DefaultMutableTreeNode)parent.getFirstChild(); + loop = SoundEvent.LIST_BOTTOM_REACHED; + } + tree.setSelectionPath(new TreePath(nextTreeNode.getPath())); + final String currentPathSpeech = currentPathSpeech(tree); + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + }}); + + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP,0),"up"); + tree.getActionMap().put("up", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); + DefaultMutableTreeNode previousTreeNode = treeNode.getPreviousSibling(); + SoundEvent loop = null; + if(previousTreeNode == null){ + DefaultMutableTreeNode parent = (DefaultMutableTreeNode)treeNode.getParent(); + if(parent == null) /* root node */ + previousTreeNode = treeNode; + else + previousTreeNode = (DefaultMutableTreeNode)parent.getLastChild(); + loop = SoundEvent.LIST_TOP_REACHED; + } + tree.setSelectionPath(new TreePath(previousTreeNode.getPath())); + final String currentPathSpeech = currentPathSpeech(tree); + SoundFactory.getInstance().play(loop, new PlayerListener(){ + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + }}); + + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0),"right"); + tree.getActionMap().put("right", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = tree.getSelectionPath(); + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)path.getLastPathComponent(); + if(treeNode.isLeaf()){ + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + else{ + tree.expandPath(path); + tree.setSelectionPath(new TreePath(((DefaultMutableTreeNode)treeNode.getFirstChild()).getPath())); + final String currentPathSpeech = currentPathSpeech(tree); + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_EXPAND,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + } + } + }); + + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0),"left"); + tree.getActionMap().put("left", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent evt) { + TreePath path = tree.getSelectionPath(); + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)path.getLastPathComponent(); + DefaultMutableTreeNode parent = (DefaultMutableTreeNode)treeNode.getParent(); + if(parent == null){/* root node */ + SoundFactory.getInstance().play(SoundEvent.ERROR); + } + else{ + TreePath newPath = new TreePath(((DefaultMutableTreeNode)parent).getPath()); + tree.setSelectionPath(newPath); + tree.collapsePath(newPath); + final String currentPathSpeech = currentPathSpeech(tree); + SoundFactory.getInstance().play(SoundEvent.TREE_NODE_COLLAPSE,new PlayerListener(){ + @Override + public void playEnded() { + NarratorFactory.getInstance().speak(currentPathSpeech); + } + }); + } + } + }); + + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,0),"space"); + tree.getActionMap().put("space",new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + space(tree); + } + }); + /* make the tree ignore the page up and page down keys */ + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,0),"none"); + tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,0),"none"); + } + + protected String currentPathSpeech(JTree tree) { + TreePath path = tree.getSelectionPath(); + DefaultMutableTreeNode selectedPathTreeNode = (DefaultMutableTreeNode)path.getLastPathComponent(); + return selectedPathTreeNode.toString(); + } + + protected void space(JTree tree){ + NarratorFactory.getInstance().speak(currentPathSpeech(tree)); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/CCmIUncaughtExceptionHandler.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,73 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The UncaughtExceptionHandler for the CCmI Editor. It logs the occurred exception stack trace + * on a file (errorN.log, where N is an integer number automatically assigned to avoid + * collision with other files of the same type) which is created in the same directory where + * the program is run. The exception stack trace will be in the format defined by the {@code XMLFormatter} class + * of the java.utli.logging package. + * + * @see java.util.logging.XMLFormatter + */ +public class CCmIUncaughtExceptionHandler implements UncaughtExceptionHandler { + + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + try{ + Logger logger = Logger.getLogger("uncaught_exception"); + logger.setLevel(Level.SEVERE); + FileHandler fileHandler = null; + try { + fileHandler = new FileHandler("error%u.log",true); + } catch (IOException e) { + System.err.println(throwable.toString()); + System.err.println(); + System.err.println("Could not use error log file"); + e.printStackTrace(); + return; + } + fileHandler.setLevel(Level.SEVERE); + logger.addHandler(fileHandler); + StringBuilder builder = new StringBuilder(throwable.toString()); + builder.append('\n'); + final Writer result = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(result); + throwable.printStackTrace(printWriter); + builder.append(result.toString()); + logger.severe(builder.toString()); + fileHandler.close(); + throwable.printStackTrace(); + }catch(Exception exception){ + exception.printStackTrace(); + System.exit(-1); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/CharEscaper.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,96 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +/** + * A utility class providing static methods to escape one or more characters. + * + */ +public class CharEscaper { + /** + * Replaces the new line character with a '|' in the {@code String} passed as argument. + * The original {@code String} can be restored by passing the returned {@code String} to + * {@link #restoreNewline(String)}. Existing '|' characters will be escaped in order not to miss + * them in the restore process. + * @param s the {@code String} to remove new line characters from + * @return a {@code String} where all new line characters have been replaced by '|' + */ + public static String replaceNewline(String s){ + String result = s.replace("|", "'|"); + return result.replace('\n', '|'); + } + + /** + * Restores a {@code String} whose new line characters have been previously replaced by + * {@link #replaceNewline(String)}, to the original form. + * @param s the string to be restored + * @return the restored string + */ + public static String restoreNewline(String s){ + String result = s.replaceAll("([^'])\\|","$1\n"); + return result.replace("'|", "|"); + } + + /** + * Escapes a set of character with another character. + * @param s The {@code String} whose characters must be escaped. + * @param charsToEscape The set of characters to escape + * @param escapeChar The escape character + * @return a new {@code String} where characters in {@code charsToEscape} have been escaped + * by {@code escapeChar}. + */ + public static String escapeCharSequence(String s, CharSequence charsToEscape, char escapeChar ){ + String result = s; + for(int i=0;i< charsToEscape.length();i++){ + char c = charsToEscape.charAt(i); + if(c == escapeChar) + throw new IllegalArgumentException("escape character cannot be in chars to escape sequence"); + for(int j=0;j<i;j++) + if(charsToEscape.charAt(j) == c) + throw new IllegalArgumentException("chars to escape sequence can only have unique characters"); + result = result.replace(""+c, ""+escapeChar+c); + } + return result; + } + + /** + * Removes an escape character preceding a set of character. + * @param s The {@code String} containing the characters to un-escape + * @param charsToUnescape The set of characters to the which must be un-escaped + * @param escapeChar The escape character + * @return A {@code String} where the escape character preceding the characters + * in charsToUnescape have been removed. + */ + public static String unescapeCharSequence(String s, CharSequence charsToUnescape, char escapeChar ){ + String result = s; + for(int i=0;i< charsToUnescape.length();i++){ + char c = charsToUnescape.charAt(i); + if(c == escapeChar) + throw new IllegalArgumentException("escape character cannot be in chars to escape sequence"); + for(int j=0;j<i;j++) + if(charsToUnescape.charAt(j) == c) + throw new IllegalArgumentException("chars to escape sequence can only have unique characters"); + result = result.replace(""+escapeChar+c,""+c); + } + return result; + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/ExceptionHandler.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,34 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +/** + * An interface implemented by classes that can handle an Exception + * + * + */ +public interface ExceptionHandler { + + /** + * Called when an exception occurs + * @param e The occurred exception + */ + void handleException(Exception e); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/GridBagUtilities.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,114 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.awt.GridBagConstraints; + +/** + * + * A Utility class providing static method to quickly arrange components, laid out by + * a GridBagLayout, in the following way: one component per row and + * either taking the whole column or just the right part of it, if preceded by a label. + */ +public class GridBagUtilities { + public GridBagUtilities(){ + labelPad = DEFAULT_LABEL_PAD; + row = 0; + } + + /** + * 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 ; + + c = new GridBagConstraints(); + c.anchor = GridBagConstraints.WEST; + c.gridx = 0; + c.gridy = row; + c.insets = new java.awt.Insets(PAD,PAD,PAD,pad); + + return c; + } + + /** + * Equivalent to {@link #label(int)} passing as argument the value previously + * set by {@link #setLabelPad(int)} or {@link #DEFAULT_LABEL_PAD} otherwise. + * + * @return a {@code GridBagConstrains} object to pass to the {@code add} method of {@code JComponent} + */ + 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; + } + + /** + * Provides the {@code GridBagConstrains} for a component placed on the same row + * and on the right of a label. + * + * @return a {@code GridBagConstrains} object to pass to the {@code add} method of {@code JComponent} + */ + public GridBagConstraints field(){ + GridBagConstraints c; + + c = new GridBagConstraints(); + c.anchor = GridBagConstraints.CENTER; + c.gridx = 1; + c.gridy = row++; + c.insets = new java.awt.Insets(PAD,PAD,PAD,PAD); + c.fill = GridBagConstraints.HORIZONTAL; + + return c; + } + + /** + * Provides the {@code GridBagConstrains} for a component placed alone on a row. + * + * @return a {@code GridBagConstrains} object to pass to the {@code add} method of {@code JComponent} + */ + public GridBagConstraints all(){ + GridBagConstraints c; + + c = new GridBagConstraints(); + c.gridy = row++; + c.anchor = GridBagConstraints.CENTER; + c.gridwidth = GridBagConstraints.REMAINDER; + c.fill = GridBagConstraints.HORIZONTAL; + c.insets = new java.awt.Insets(PAD,PAD,PAD,PAD); + return c; + } + + private int labelPad; + private int row; + public static final int DEFAULT_LABEL_PAD = 50; + public static final int PAD = 2; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/InteractionLog.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,133 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * A logger class using the {@code java.util.logging} package to log all the user's + * relevant actions. + */ +public class InteractionLog { + /** + * Enable the logging + * @param logFileDir the path of the directory where the log file will be saved + * @throws IOException if a I/O problem occurs when writing log entry to the log file + */ + public static void enable(String logFileDir) throws IOException{ + logger.setLevel(Level.FINE); + logger.setUseParentHandlers(false); + if(fileHandler == null){ + fileHandler = new FileHandler(logFileDir+System.getProperty("file.separator")+"interaction%u.log",true); + fileHandler.setFormatter(new InteractionLogFormatter()); + logger.addHandler(fileHandler); + + /* also print the log on the console */ + java.util.logging.ConsoleHandler ch = new java.util.logging.ConsoleHandler(); + ch.setLevel(Level.ALL); + ch.setFormatter(new InteractionLogFormatter()); + logger.addHandler(ch); + } + } + + /** + * Disable the logging + */ + public static void disable(){ + logger.setLevel(Level.OFF); + } + + /** + * Logs a log entry in the file. Log entries are action that occurred during + * the interaction by local or remote user. + * + * @param source the source of the interaction + * @param action the occurred action + * @param args further informations about the occurred action + */ + public static void log(String source, String action, String args){ + StringBuilder builder = new StringBuilder(source); + builder.append(SEPARATOR) + .append(action) + .append(SEPARATOR) + .append(args); + + logger.fine(builder.toString()); + } + + /** + * Logs a general message in the log file. This log entries are not + * linked to specific action of a local or remote user. + * @param msg the message to log + */ + public static void log(String msg){ + logger.config(msg); + } + + private static class InteractionLogFormatter extends Formatter{ + + private InteractionLogFormatter(){ + super(); + } + + @Override + public String format(LogRecord record) { + StringBuilder builder = new StringBuilder(); + if(record.getLevel() == Level.CONFIG){ + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss"); + builder.append("--- ") + .append(dateFormat.format(new Date(record.getMillis()))) + .append(" - ") + .append(record.getMessage()) + .append(" ---") + .append(NEW_LINE); + }else if(record.getLevel() == Level.FINE){ + SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss.SSS"); + builder.append(dateFormat.format(new Date(record.getMillis()))) + .append(SEPARATOR) + .append(record.getMessage()) + .append(NEW_LINE); + } + + return builder.toString(); + } + } + + /** + * Release allocated resources. to be called hwen the interaction log is + * no longer needed. + */ + public static void dispose(){ + if(fileHandler != null) + fileHandler.close(); + } + + private static Logger logger = Logger.getLogger("interaction"); + private static FileHandler fileHandler; + private static char SEPARATOR = ','; + private final static String NEW_LINE = System.getProperty("line.separator"); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/OsDetector.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,47 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +/** + * + * Provides static methods to probe which Operating System we're running on + * + */ +public class OsDetector { + + public static boolean isWindows(){ + return(OS.indexOf( "win" ) >= 0); + } + + public static boolean isMac(){ + return(OS.indexOf( "mac" ) >= 0); + } + + public static boolean isUnix(){ + return(OS.indexOf( "nix") >=0 || OS.indexOf( "nux") >=0); + } + + public static boolean has64BitJVM(){ + String dataModel = System.getProperty("sun.arch.data.model"); + return dataModel.equals("64"); + } + + static final String OS = System.getProperty("os.name").toLowerCase(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/Pair.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,75 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +/** + * + * A Pair of objects. + * + * @param <T1> The first item type + * @param <T2> The second item type + */ +public class Pair<T1,T2> { + public Pair(T1 first, T2 second){ + this.first = first; + this.second = second; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((first == null) ? 0 : first.hashCode()); + result = prime * result + ((second == null) ? 0 : second.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + @SuppressWarnings("rawtypes") + Pair other = (Pair) obj; + if (first == null) { + if (other.first != null) + return false; + } else if (!first.equals(other.first)) + return false; + if (second == null) { + if (other.second != null) + return false; + } else if (!second.equals(other.second)) + return false; + return true; + } + + /** + * the first item of the pair + */ + public T1 first; + /** + * the second item of the pair + */ + public T2 second; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/PreferencesService.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,77 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.util.prefs.Preferences; + +/** + * A service for storing and loading user preferences. + */ +public abstract class PreferencesService +{ + /** + * Gets an instance of the service, suitable for the package of the given class. + * @return an instance of the service + */ + public static PreferencesService getInstance(){ + if (service != null) return service; + try{ + service = new DefaultPreferencesService(); + return service; + } + catch (SecurityException exception){ + throw new RuntimeException(exception); + } + } + + /** + * Gets a previously stored string from the service. + * @param key the key of the string + * @param defval the value to return if no matching value was found + * @return the value stored with the given key, or defval if none was found + */ + public abstract String get(String key, String defval); + /** + * Saves a key/value pair for later retrieval. + * @param key the key of the string to be stored + * @param value the value to to be stored + */ + public abstract void put(String key, String value); + + private static PreferencesService service; +} + +/** + * The default preferences service that uses the java.util.prefs API. + */ +class DefaultPreferencesService extends PreferencesService{ + + public DefaultPreferencesService(){ + prefs = Preferences.userNodeForPackage(this.getClass()); + } + + @Override + public String get(String key, String defval) { return prefs.get(key, defval); } + @Override + public void put(String key, String defval) { prefs.put(key, defval); } + + private Preferences prefs; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/utils/ResourceFileWriter.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,119 @@ +/* + CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool + + Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package uk.ac.qmul.eecs.ccmi.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** + * This class is used to store a resource (e.g. .dll file in windows)to the local file system. + * + * This class can be used to allow the virtual machine to load resources which come embedded + * into a jar file. + */ +public class ResourceFileWriter { + + /** + * Creates an instance of the the class linked to no resource. an resource file writer + * created with this constructor is useless, unless {@code setResource} is called before + * writing the resource on the disk. + * + * @see Class#getResource(String) + */ + public ResourceFileWriter(){ + } + + /** + * Creates an instance of the the class linked to a specific resource + * + * @param resource the resource URL. The URL can be obtained by + * {@code getResource}, therefore can be called from a class within a jar file + * which needs to access embedded resources. + */ + public ResourceFileWriter(URL resource){ + this.resource = resource; + } + + /** + * Sets a new resource for this resource file writer. Successive calls to {@code writeToDisk} + * will store the resouce passed as argument in the file system. + * + * @param resource the URL of the new resource this resource file writer is linked to + */ + public void setResource(URL resource){ + this.resource = resource;; + path = null; + } + + /** + * Writes the resource in the file passed as argument. + * + * The path to the file can be retrieved afterwards through @see {@link #getFilePath()} + * + * @param dir the directory where the resource will be saved + * @param fileName the name of the file the resoruce will be saved in + */ + public void writeOnDisk(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){ioe.printStackTrace();} + if(out != null) + try{out.close();}catch(IOException ioe){ioe.printStackTrace();} + } + + } + + /** + * 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} if no resource was set for this + * resource file writer or {@code writeToDisk} was unsuccessful + */ + public String getFilePath(){ + return path; + } + + private URL resource; + private String path; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/license.txt Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. \ No newline at end of file