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;}