f@0: /* f@0: CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool f@0: f@0: Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) f@0: f@0: This program is free software: you can redistribute it and/or modify f@0: it under the terms of the GNU General Public License as published by f@0: the Free Software Foundation, either version 3 of the License, or f@0: (at your option) any later version. f@0: f@0: This program is distributed in the hope that it will be useful, f@0: but WITHOUT ANY WARRANTY; without even the implied warranty of f@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the f@0: GNU General Public License for more details. f@0: f@0: You should have received a copy of the GNU General Public License f@0: along with this program. If not, see . f@0: */ f@0: package uk.ac.qmul.eecs.ccmi.diagrammodel; f@0: f@0: import java.util.ArrayList; f@0: import java.util.Collection; f@0: import java.util.Collections; f@0: import java.util.Enumeration; f@0: import java.util.LinkedHashMap; f@0: import java.util.LinkedHashSet; f@0: import java.util.LinkedList; f@0: import java.util.List; f@0: import java.util.Map; f@0: import java.util.Set; f@0: import java.util.concurrent.locks.ReentrantLock; f@0: f@0: import javax.swing.event.ChangeEvent; f@0: import javax.swing.event.ChangeListener; f@0: import javax.swing.tree.DefaultTreeModel; f@0: import javax.swing.tree.MutableTreeNode; f@0: import javax.swing.tree.TreeNode; f@0: f@0: import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; f@0: import uk.ac.qmul.eecs.ccmi.utils.InteractionLog; f@0: f@0: /** f@0: * This class represent a model as per in the model-view control architecture. f@0: * The model is "double sided" in the sense that it can be accessed through either f@0: * a CollectionModel or a TreeModel returned by the respective getter methods. f@0: * The TreeModel is suitable for JTree classes of the swing library, while f@0: * the CollectionModel can be used by view classes by registering a CollectionListener f@0: * to the CollectionModel itself. f@0: * It is important to notice that changes made on one side will reflect on the other, f@0: * eventually triggering the registered listeners. f@0: * The tree model is structured according to a special layout which is suitable for f@0: * browsing the tree view via audio interface ( text to speech synthesis and sound). f@0: * f@0: * @param a subclass of DiagramNode f@0: * @param a subclass of DiagramEdge f@0: */ f@0: public class DiagramModel{ f@0: /** f@0: * Create a model instance starting from some nodes and edges prototypes. f@0: * All subsequently added element must be clones of such prototypes. f@0: * @param nodePrototypes an array of {@code DiagramNode} prototypes, from which f@0: * nodes that will be inserted in this model will be cloned f@0: * @param edgePrototypes an array of {@code DiagramEdge} prototypes, from which f@0: * edges that will be inserted in this model will be cloned f@0: */ f@0: @SuppressWarnings("serial") f@0: public DiagramModel(N [] nodePrototypes, E [] edgePrototypes) { f@0: root = new DiagramTreeNode(ROOT_LABEL){ f@0: @Override f@0: public boolean isRoot(){ f@0: return true; f@0: } f@0: }; f@0: modified = false; f@0: f@0: nodeCounter = 0; f@0: edgeCounter = 0; f@0: f@0: notifier = new ReentrantLockNotifier(); f@0: f@0: treeModel = new InnerTreeModel(root); f@0: treeModel.setEventSource(treeModel);/* default event source is the tree itself */ f@0: diagramCollection = new InnerDiagramCollection(); f@0: f@0: nodes = new ArrayList(INITIAL_NODES_SIZE); f@0: edges = new ArrayList(INITIAL_EDGES_SIZE); f@0: elements = new ArrayList(INITIAL_NODES_SIZE+INITIAL_EDGES_SIZE); f@0: f@0: changeListeners = new LinkedList(); f@0: f@0: for(N n : nodePrototypes) f@0: addType(n); f@0: for(E e : edgePrototypes){ f@0: addType(e); f@0: } f@0: } f@0: f@0: /** f@0: * Returns a CollectionModel for this diagram f@0: * f@0: * @return a CollectionModel for this diagram f@0: */ f@0: public CollectionModel getDiagramCollection(){ f@0: return diagramCollection; f@0: } f@0: f@0: /** f@0: * Returns a TreeModel for this diagram f@0: * f@0: * @return a TreeModel for this diagram f@0: */ f@0: public TreeModel getTreeModel(){ f@0: return treeModel; f@0: } f@0: f@0: private void handleChangeListeners(Object source){ f@0: if(modified) // fire the listener only the first time a change happens f@0: return; f@0: modified = true; f@0: fireChangeListeners(source); f@0: } f@0: f@0: private void addChangeListener(ChangeListener l){ f@0: changeListeners.add(l); f@0: } f@0: f@0: private void removeChangeListener(ChangeListener l){ f@0: changeListeners.remove(l); f@0: } f@0: f@0: protected void fireChangeListeners(Object source){ f@0: ChangeEvent changeEvent = new ChangeEvent(source); f@0: for(ChangeListener l : changeListeners) f@0: l.stateChanged(changeEvent); f@0: } f@0: f@0: private void addType(DiagramElement element){ f@0: DiagramTreeNode typeNode = _lookForChild(root, element.getType()); f@0: if(typeNode == null){ f@0: typeNode = new TypeMutableTreeNode(element); f@0: treeModel.insertNodeInto(typeNode, root, root.getChildCount()); f@0: } f@0: } f@0: f@0: private class InnerDiagramCollection implements CollectionModel { f@0: f@0: public InnerDiagramCollection(){ f@0: listeners = new ArrayList(); f@0: } f@0: f@0: @Override f@0: public boolean insert(N n, Object source){ f@0: if(source == null) f@0: source = this; f@0: return _insert(n,source); f@0: } f@0: f@0: @Override f@0: public boolean insert(E e, Object source){ f@0: if(source == null) f@0: source = this; f@0: return _insert(e,source); f@0: } f@0: f@0: @Override f@0: public boolean takeOut(DiagramElement element, Object source){ f@0: if(source == null) f@0: source = this; f@0: if(element instanceof DiagramNode) f@0: return _takeOut((DiagramNode)element,source); f@0: if(element instanceof DiagramEdge) f@0: return _takeOut((DiagramEdge)element,source); f@0: return false; f@0: } f@0: f@0: @Override f@0: public void addCollectionListener(CollectionListener listener) { f@0: listeners.add(listener); f@0: } f@0: f@0: @Override f@0: public void removeCollectionListener(CollectionListener listener) { f@0: listeners.remove(listener); f@0: } f@0: f@0: protected void fireElementInserted(Object source, DiagramElement element) { f@0: for(CollectionListener l : listeners){ f@0: l.elementInserted(new CollectionEvent(source,element)); f@0: } f@0: } f@0: f@0: protected void fireElementTakenOut(Object source, DiagramElement element) { f@0: for(CollectionListener l : listeners){ f@0: l.elementTakenOut(new CollectionEvent(source,element)); f@0: } f@0: } f@0: f@0: protected void fireElementChanged(ElementChangedEvent evt){ f@0: for(CollectionListener l : listeners){ f@0: l.elementChanged(evt); f@0: } f@0: } f@0: f@0: @Override f@0: public Collection getNodes() { f@0: return Collections.unmodifiableCollection(nodes); f@0: } f@0: f@0: @Override f@0: public Collection getEdges() { f@0: return Collections.unmodifiableCollection(edges); f@0: } f@0: f@0: @Override f@0: public Collection getElements(){ f@0: return Collections.unmodifiableCollection(elements); f@0: } f@0: f@0: @Override f@0: public ReentrantLock getMonitor(){ f@0: return notifier; f@0: } f@0: f@0: @Override f@0: public void addChangeListener(ChangeListener l){ f@0: DiagramModel.this.addChangeListener(l); f@0: } f@0: f@0: @Override f@0: public void removeChangeListener(ChangeListener l){ f@0: DiagramModel.this.removeChangeListener(l); f@0: } f@0: f@0: /* sort the collections according to the id of nodes */ f@0: public void sort(){ f@0: Collections.sort(nodes, DiagramElementComparator.getInstance()); f@0: Collections.sort(edges, DiagramElementComparator.getInstance()); f@0: } f@0: f@0: public boolean isModified(){ f@0: return modified; f@0: } f@0: f@0: public void setUnmodified(){ f@0: modified = false; f@0: } f@0: f@0: protected ArrayList listeners; f@0: f@0: } f@0: f@0: @SuppressWarnings("serial") f@0: private class InnerTreeModel extends DefaultTreeModel implements TreeModel{ f@0: f@0: public InnerTreeModel(TreeNode root){ f@0: super(root); f@0: bookmarks = new LinkedHashMap(); f@0: diagramTreeNodeListeners = new ArrayList(); f@0: } f@0: f@0: @Override f@0: public boolean insertTreeNode(N treeNode, Object source){ f@0: if(source == null) f@0: source = this; f@0: return _insert(treeNode,source); f@0: } f@0: f@0: @Override f@0: public boolean insertTreeNode(E treeNode, Object source){ f@0: if(source == null) f@0: source = this; f@0: return _insert(treeNode,source); f@0: } f@0: f@0: @Override f@0: public boolean takeTreeNodeOut(DiagramElement treeNode, Object source){ f@0: if(source == null) f@0: source = this; f@0: boolean result; f@0: if(treeNode instanceof DiagramEdge){ f@0: result = _takeOut((DiagramEdge)treeNode,source); f@0: } f@0: else{ f@0: result = _takeOut((DiagramNode)treeNode,source); f@0: } f@0: /* remove the bookmarks associated with the just deleted diagram element, if any */ f@0: for(String key : treeNode.getBookmarkKeys()) f@0: bookmarks.remove(key); f@0: return result; f@0: } f@0: f@0: @Override f@0: public DiagramTreeNode putBookmark(String bookmark, DiagramTreeNode treeNode, Object source){ f@0: if(bookmark == null) f@0: throw new IllegalArgumentException("bookmark cannot be null"); f@0: if(source == null) f@0: source = this; f@0: setEventSource(source); f@0: treeNode.addBookmarkKey(bookmark); f@0: DiagramTreeNode result = bookmarks.put(bookmark, treeNode); f@0: nodeChanged(treeNode); f@0: iLog("bookmark added",bookmark); f@0: DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,bookmark,source); f@0: for(DiagramTreeNodeListener l : diagramTreeNodeListeners){ f@0: l.bookmarkAdded(evt); f@0: } f@0: handleChangeListeners(this); f@0: return result; f@0: } f@0: f@0: @Override f@0: public DiagramTreeNode getBookmarkedTreeNode(String bookmark) { f@0: return bookmarks.get(bookmark); f@0: } f@0: f@0: @Override f@0: public DiagramTreeNode removeBookmark(String bookmark,Object source) { f@0: if(source == null) f@0: source = this; f@0: setEventSource(source); f@0: DiagramTreeNode treeNode = bookmarks.remove(bookmark); f@0: treeNode.removeBookmarkKey(bookmark); f@0: nodeChanged(treeNode); f@0: iLog("bookmark removed",bookmark); f@0: DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,bookmark,source); f@0: for(DiagramTreeNodeListener l : diagramTreeNodeListeners){ f@0: l.bookmarkRemoved(evt); f@0: } f@0: handleChangeListeners(this); f@0: return treeNode; f@0: } f@0: f@0: @Override f@0: public Set getBookmarks(){ f@0: return new LinkedHashSet(bookmarks.keySet()); f@0: } f@0: f@0: @Override f@0: public void setNotes(DiagramTreeNode treeNode, String notes,Object source){ f@0: if(source == null) f@0: source = this; f@0: setEventSource(source); f@0: String oldValue = treeNode.getNotes(); f@0: treeNode.setNotes(notes,source); f@0: nodeChanged(treeNode); f@0: iLog("notes set for "+treeNode.getName(),"".equals(notes) ? "empty notes" : notes.replaceAll("\n", "\\\\n")); f@0: DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,oldValue,source); f@0: for(DiagramTreeNodeListener l : diagramTreeNodeListeners){ f@0: l.notesChanged(evt); f@0: } f@0: handleChangeListeners(source); f@0: } f@0: f@0: private void setEventSource(Object source){ f@0: this.src = source; f@0: } f@0: f@0: @Override f@0: public ReentrantLock getMonitor(){ f@0: return notifier; f@0: } f@0: f@0: @Override f@0: public void addDiagramTreeNodeListener(DiagramTreeNodeListener l){ f@0: diagramTreeNodeListeners.add(l); f@0: } f@0: f@0: @Override f@0: public void removeDiagramTreeNodeListener(DiagramTreeNodeListener l){ f@0: diagramTreeNodeListeners.remove(l); f@0: } f@0: f@0: /* redefine the fire methods so that they set the source object according */ f@0: /* to whether the element was inserted from the graph or from the tree */ f@0: @Override f@0: protected void fireTreeNodesChanged(Object source, Object[] path, f@0: int[] childIndices, Object[] children) { f@0: super.fireTreeNodesChanged(src, path, childIndices, children); f@0: } f@0: f@0: @Override f@0: protected void fireTreeNodesInserted(Object source, Object[] path, f@0: int[] childIndices, Object[] children) { f@0: super.fireTreeNodesInserted(src, path, childIndices, children); f@0: } f@0: f@0: @Override f@0: protected void fireTreeNodesRemoved(Object source, Object[] path, f@0: int[] childIndices, Object[] children) { f@0: super.fireTreeNodesRemoved(src, path, childIndices, children); f@0: } f@0: f@0: @Override f@0: protected void fireTreeStructureChanged(Object source, Object[] path, f@0: int[] childIndices, Object[] children) { f@0: super.fireTreeStructureChanged(src, path, childIndices, children); f@0: } f@0: f@0: public boolean isModified(){ f@0: return modified; f@0: } f@0: f@0: public void setUnmodified(){ f@0: modified = false; f@0: } f@0: f@0: private Object src; f@0: private Map bookmarks; f@0: private ArrayList diagramTreeNodeListeners; f@0: } f@0: f@0: @SuppressWarnings("serial") f@0: class ReentrantLockNotifier extends ReentrantLock implements ElementNotifier { f@0: @Override f@0: public void notifyChange(ElementChangedEvent evt) { f@0: _change(evt); f@0: handleChangeListeners(evt.getDiagramElement()); f@0: } f@0: } f@0: f@0: private boolean _insert(N n, Object source) { f@0: assert(n != null); f@0: f@0: /* if id has already been given then sync the counter so that a surely new value is given to the next nodes */ f@0: if(n.getId() == DiagramElement.NO_ID) f@0: n.setId(++nodeCounter); f@0: else if(n.getId() > nodeCounter) f@0: nodeCounter = n.getId(); f@0: f@0: treeModel.setEventSource(source); f@0: nodes.add(n); f@0: elements.add(n); f@0: /* add the node to outer node's (if any) inner nodes */ f@0: if(n.getExternalNode() != null) f@0: n.getExternalNode().addInternalNode(n); f@0: f@0: /* decide where to insert the node based on whether this is an inner node or not */ f@0: MutableTreeNode parent; f@0: if(n.getExternalNode() == null){ f@0: DiagramTreeNode typeNode = _lookForChild(root, n.getType()); f@0: if(typeNode == null) f@0: throw new IllegalArgumentException("Node type "+n.getType()+" not present in the model"); f@0: parent = typeNode; f@0: }else{ f@0: parent = n.getExternalNode(); f@0: } f@0: f@0: /* add to the node one child per property type */ f@0: for(String propertyType : n.getProperties().getTypes()) f@0: n.insert(new PropertyTypeMutableTreeNode(propertyType,n), n.getChildCount()); f@0: f@0: /* inject the notifier for managing changes internal to the edge */ f@0: n.setNotifier(notifier); f@0: f@0: /* insert node into tree which fires tree listeners */ f@0: treeModel.insertNodeInto(n, parent, parent.getChildCount()); f@0: /* this is necessary to increment the child counter displayed between brackets */ f@0: treeModel.nodeChanged(parent); f@0: diagramCollection.fireElementInserted(source,n); f@0: handleChangeListeners(n); f@0: f@0: iLog("node inserted",DiagramElement.toLogString(n)); f@0: return true; f@0: } f@0: f@0: private boolean _takeOut(DiagramNode n, Object source) { f@0: treeModel.setEventSource(source); f@0: /* recursively remove internal nodes of this node */ f@0: _removeInternalNodes(n,source); f@0: /* clear external node and clear edges attached to this node and updates other ends of such edges */ f@0: _clearNodeReferences(n,source); f@0: /* remove the node from the tree (fires listeners) */ f@0: treeModel.removeNodeFromParent(n); f@0: /* this is necessary to increment the child counter displayed between brackets */ f@0: treeModel.nodeChanged(n.getParent()); f@0: /* remove the nodes from the collection */ f@0: nodes.remove(n); f@0: elements.remove(n); f@0: /* notify all the listeners a new node has been removed */ f@0: diagramCollection.fireElementTakenOut(source,n); f@0: handleChangeListeners(n); f@0: f@0: if(nodes.isEmpty()){ f@0: nodeCounter = 0; f@0: }else{ f@0: long lastNodeId = nodes.get(nodes.size()-1).getId(); f@0: if(n.getId() > lastNodeId) f@0: nodeCounter = lastNodeId; f@0: } f@0: iLog("node removed",DiagramElement.toLogString(n)); f@0: return true; f@0: } f@0: f@0: private boolean _insert(E e, Object source) { f@0: assert(e != null); f@0: /* executes formal controls over the edge's node, which must be specified from the outer class*/ f@0: if(e.getNodesNum() < 2) f@0: throw new MalformedEdgeException("too few (" +e.getNodesNum()+ ") nodes"); f@0: f@0: /* if id has already been given then sync the counter so that a surely new value is given to the next edges */ f@0: if(e.getId() > edgeCounter) f@0: edgeCounter = e.getId(); f@0: else f@0: e.setId(++edgeCounter); f@0: f@0: treeModel.setEventSource(source); f@0: edges.add(e); f@0: elements.add(e); f@0: f@0: /* updates the nodes' edge reference and the edge tree references */ f@0: for(int i = e.getNodesNum()-1; i >= 0; i--){ f@0: DiagramNode n = e.getNodeAt(i); f@0: assert(n != null); f@0: /* insert first the type of the edge, if not already present */ f@0: DiagramTreeNode edgeType = _lookForChild(n, e.getType()); f@0: if(edgeType == null){ f@0: edgeType = new EdgeReferenceHolderMutableTreeNode(e.getType()); f@0: treeModel.insertNodeInto(edgeType, n, 0); f@0: } f@0: f@0: /* insert the edge reference under its type tree node, in the node*/ f@0: treeModel.insertNodeInto(new EdgeReferenceMutableTreeNode(e,n), edgeType, 0); f@0: /* this is necessary to increment the child counter displayed between brackets */ f@0: treeModel.nodeChanged(edgeType); f@0: f@0: n.addEdge(e); f@0: /* insert the node reference into the edge tree node */ f@0: e.insert(new NodeReferenceMutableTreeNode(n,e), 0); f@0: } f@0: f@0: DiagramTreeNode parent = _lookForChild(root, e.getType()); f@0: if(parent == null) f@0: throw new IllegalArgumentException("Edge type "+e.getType()+" not present in the model"); f@0: f@0: /* inject the controller and notifier to manage changes internal to the edge */ f@0: e.setNotifier(notifier); f@0: f@0: /* c'mon baby light my fire */ f@0: treeModel.insertNodeInto(e, parent, parent.getChildCount()); f@0: /* this is necessary to increment the child counter displayed between brackets */ f@0: treeModel.nodeChanged(parent); f@0: diagramCollection.fireElementInserted(source,e); f@0: handleChangeListeners(e); f@0: f@0: StringBuilder builder = new StringBuilder(DiagramElement.toLogString(e)); f@0: builder.append(" connecting:"); f@0: for(int i=0; i lastEdgeId) f@0: edgeCounter = lastEdgeId; f@0: } f@0: iLog("edge removed",DiagramElement.toLogString(e)); f@0: return true; f@0: } f@0: f@0: private void _removeInternalNodes(DiagramNode n, Object source){ f@0: for(int i=0; i edgesToRemove = new ArrayList(edges.size()); f@0: for(int i=0; i empty = Collections.emptyList(); f@0: for(int i=0; i children = parentNode.children(); children.hasMoreElements();){ f@0: temp = children.nextElement(); f@0: if(temp.getName().equals(name)){ f@0: child = temp; f@0: break; f@0: } f@0: } f@0: return child; f@0: } f@0: f@0: private static NodeReferenceMutableTreeNode _lookForNodeReference(DiagramEdge parent, DiagramNode n){ f@0: NodeReferenceMutableTreeNode child = null, temp; f@0: for(@SuppressWarnings("unchecked") f@0: Enumeration children = parent.children(); children.hasMoreElements();){ f@0: temp = (NodeReferenceMutableTreeNode)children.nextElement(); f@0: if( ((NodeReferenceMutableTreeNode)temp).getNode().equals(n)){ f@0: child = temp; f@0: break; f@0: } f@0: } f@0: return child; f@0: } f@0: f@0: private static EdgeReferenceMutableTreeNode _lookForEdgeReference( DiagramNode parentNode, DiagramEdge e){ f@0: DiagramTreeNode edgeType = _lookForChild(parentNode, e.getType()); f@0: assert(edgeType != null); f@0: EdgeReferenceMutableTreeNode child = null, temp; f@0: for(@SuppressWarnings("unchecked") f@0: Enumeration children = edgeType.children(); children.hasMoreElements();){ f@0: temp = (EdgeReferenceMutableTreeNode)children.nextElement(); f@0: if( ((EdgeReferenceMutableTreeNode)temp).getEdge().equals(e)){ f@0: child = temp; f@0: break; f@0: } f@0: } f@0: return child; f@0: } f@0: f@0: private void iLog(String action,String args){ f@0: InteractionLog.log("MODEL",action,args); f@0: } f@0: f@0: private DiagramTreeNode root; f@0: private InnerDiagramCollection diagramCollection; f@0: private ArrayList nodes; f@0: private ArrayList edges; f@0: private ArrayList elements; f@0: private InnerTreeModel treeModel; f@0: f@0: private long edgeCounter; f@0: private long nodeCounter; f@0: f@0: private ReentrantLockNotifier notifier; f@0: private List changeListeners; f@0: f@0: private boolean modified; f@0: f@0: private final static String ROOT_LABEL = "Diagram"; f@0: private final static int INITIAL_EDGES_SIZE = 20; f@0: private final static int INITIAL_NODES_SIZE = 30;}