view java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramTree.java @ 0:9418ab7b7f3f

Initial import
author Fiore Martin <fiore@eecs.qmul.ac.uk>
date Fri, 16 Dec 2011 17:35:51 +0000
parents
children 4b2f975e35fa
line wrap: on
line source
/*  
 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.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
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.DiagramModelTreeNode;
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.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 {
	public DiagramTree(Diagram diagram){
		super(diagram.getTreeModel());
		this.diagram = diagram;
		resources = ResourceBundle.getBundle(EditorFrame.class.getName());
		
		TreePath rootPath = new TreePath((DiagramModelTreeNode)diagram.getTreeModel().getRoot());
		setSelectionPath(rootPath);
		collapsePath(rootPath);
		selectedNodes = new ArrayList<Node>();
		setEditable(false);
	    getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
	    overwriteTreeKeystrokes();
	    this.addTreeSelectionListener(new TreeSelectionListener(){
	    	@Override
	    	public void valueChanged(TreeSelectionEvent evt) {
	    		if(treeSelectionListenerGateOpen){
	    			final DiagramModelTreeNode treeNode = (DiagramModelTreeNode)evt.getPath().getLastPathComponent();
	    			if(treeNode instanceof DiagramElement){
	    				SoundFactory.getInstance().play(((DiagramElement)treeNode).getSound(), new PlayerListener(){
	    					@Override
	    					public void playEnded() {
	    						NarratorFactory.getInstance().speak(treeNode.spokenText());
	    					}
	    				});
	    			}else{
	    				NarratorFactory.getInstance().speak(treeNode.spokenText());
	    			}
	    		}
	    	}
		});
	    /* don't use the swing focus system as we provide one on our own */
	    setFocusTraversalKeysEnabled(false);
	    getAccessibleContext().setAccessibleName("");
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public TreeModel<Node,Edge> getModel(){
		return (TreeModel<Node,Edge>)super.getModel();
	}
	
	public void setModel(TreeModel<Node,Edge> newModel){
		DiagramModelTreeNode selectedTreeNode = (DiagramModelTreeNode)getSelectionPath().getLastPathComponent();
		super.setModel(newModel);
		collapseRow(0);
		setSelectionPath(new TreePath(selectedTreeNode.getPath()));
	}
	
	public void setDiagram(Diagram diagram){
		this.diagram = diagram;
		setModel(diagram.getTreeModel());
	}
	
	public 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());
	}
	
	public 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));
	}
	
	public DiagramNode[] getSelectedNodes(){
		DiagramNode[] array = new DiagramNode[selectedNodes.size()]; 
		return selectedNodes.toArray(array);
	}
	
	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);
		}
	}
	
	public String currentPathSpeech(){
		TreePath path =	getSelectionPath();
		DiagramModelTreeNode selectedPathTreeNode = (DiagramModelTreeNode)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();
	}
	
	/**
	 * this method changes the selected tree node from an edge/node reference
	 * to the related edge/node itself
	 */
	public void jump(JumpTo jumpTo){
		final Narrator narrator = NarratorFactory.getInstance();
		TreePath oldPath;
		treeSelectionListenerGateOpen = false;
		switch(jumpTo){
		case REFERENCE :
			oldPath = getSelectionPath();
			DiagramModelTreeNode selectedTreeNode = (DiagramModelTreeNode)oldPath.getLastPathComponent();
			if(selectedTreeNode instanceof NodeReferenceMutableTreeNode){
				final Node n = (Node)((NodeReferenceMutableTreeNode)selectedTreeNode).getNode();
				setSelectionPath(new TreePath(n.getPath()));
				SoundFactory.getInstance().setPlayerListener(new PlayerListener(){
					   @Override
					   public void playEnded() {
						   narrator.speak(MessageFormat.format(resources.getString("speech.jump"),n.spokenText()));
					   }
				   }, SoundEvent.JUMP);
			}else if(selectedTreeNode instanceof EdgeReferenceMutableTreeNode){
				final Edge e = (Edge)((EdgeReferenceMutableTreeNode)selectedTreeNode).getEdge();
				setSelectionPath(new TreePath(e.getPath()));
				SoundFactory.getInstance().setPlayerListener(new PlayerListener(){
					   @Override
					   public void playEnded() {
						   narrator.speak(MessageFormat.format(resources.getString("speech.jump"),e.spokenText()));
					   }
				   }, SoundEvent.JUMP);
			}
			/* assume the referee has only root in common with the reference and collapse everything up to the root (excluded) */
			collapseAll(selectedTreeNode, (DiagramModelTreeNode)selectedTreeNode.getPath()[1]);
			break;
		case ROOT :
			final DiagramModelTreeNode from =(DiagramModelTreeNode)getSelectionPath().getLastPathComponent();
			setSelectionRow(0);
			collapseAll(from,from.getRoot());
			SoundFactory.getInstance().setPlayerListener(new PlayerListener(){
				   @Override
				   public void playEnded() {
					   narrator.speak(MessageFormat.format(resources.getString("speech.jump"),from.getRoot().spokenText()));
				   }
			   }, SoundEvent.JUMP);
			break;
		case TYPE : // jumps to the ancestor type node of the current node, never used
			oldPath = getSelectionPath();
			int index = 0;
			Object[] pathComponents = oldPath.getPath();
			for(int i=0;i<pathComponents.length;i++){
				if(pathComponents[i] instanceof TypeMutableTreeNode){
					index=i;
					break;
				}
			}
			final DiagramModelTreeNode typeTreeNode = (DiagramModelTreeNode)oldPath.getPathComponent(index); 
			setSelectionPath(new TreePath(typeTreeNode.getPath()));
			collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(),typeTreeNode);
			SoundFactory.getInstance().setPlayerListener(new PlayerListener(){
				   @Override
				   public void playEnded() {
					   narrator.speak(MessageFormat.format(resources.getString("speech.jump"),typeTreeNode.spokenText()));
				   }
			   }, SoundEvent.JUMP);
			break;
		case SELECTED_TYPE :
			DiagramModelTreeNode root = (DiagramModelTreeNode)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","");
				treeSelectionListenerGateOpen = true;
				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((DiagramModelTreeNode)oldPath.getLastPathComponent(), (DiagramModelTreeNode)initialValue);
			SoundFactory.getInstance().setPlayerListener(new PlayerListener(){
				   @Override
				   public void playEnded() {
					   narrator.speak(MessageFormat.format(resources.getString("speech.jump"),selectedValue));
				   }
			   }, SoundEvent.JUMP);
			break;
		case BOOKMARK :
			TreeModel<Node,Edge> treeModel = getModel(); 
			
			if(treeModel.getBookmarks().size() == 0){
				SoundFactory.getInstance().play(SoundEvent.ERROR ,new PlayerListener(){
					   @Override
					   public void playEnded() {
						   narrator.speak("speech.no_bookmarks");
					   }
				   });
				InteractionLog.log(INTERACTIONLOG_SOURCE,"no bookmarks available","");
				treeSelectionListenerGateOpen = true;
				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();
				DiagramModelTreeNode treeNode = treeModel.getBookmarkedTreeNode(bookmark);
				collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(), (DiagramModelTreeNode)treeModel.getRoot());
				setSelectionPath(new TreePath(treeNode.getPath()));
				final String currentPathSpeech = currentPathSpeech();
				SoundFactory.getInstance().setPlayerListener(new PlayerListener(){
					   @Override
					   public void playEnded() {
						   narrator.speak(currentPathSpeech);
					   }
				   }, SoundEvent.JUMP);
				InteractionLog.log(INTERACTIONLOG_SOURCE,"bookmark selected",bookmark);
			}else{
				/* it speaks anyway, as we set up the speech in the EditorFrame class. no need to use the narrator then */
				SoundFactory.getInstance().play(SoundEvent.CANCEL);
				InteractionLog.log(INTERACTIONLOG_SOURCE,"cancel select bookmark dialog","");
				treeSelectionListenerGateOpen = true;
				return;
			}
			break;
			
		}
		InteractionLog.log(INTERACTIONLOG_SOURCE,"jumped to "+jumpTo.toString(),((DiagramModelTreeNode)getSelectionPath().getLastPathComponent()).getName());
		SoundFactory.getInstance().play(SoundEvent.JUMP);
		treeSelectionListenerGateOpen = true;
	}
	
	public void jumpTo(final DiagramElement de){
		TreePath oldPath = getSelectionPath();
		collapseAll((DiagramModelTreeNode)oldPath.getLastPathComponent(),de);
		setSelectionPath(new TreePath(de.getPath()));
		treeSelectionListenerGateOpen = false;
		SoundFactory.getInstance().play( SoundEvent.JUMP, new PlayerListener(){
			@Override
			public void playEnded() {
				NarratorFactory.getInstance().speak(MessageFormat.format(resources.getString("speech.jump"),de.spokenText()));
			}
		});
		treeSelectionListenerGateOpen = true;
	}
	
	/* collapse all the nodes in the path from "from" to "to" upwards(with the same direction as going from a leaf to the root)*/
	private void collapseAll(DiagramModelTreeNode from, DiagramModelTreeNode to){
		DiagramModelTreeNode currentNode = from;
		while(currentNode.getParent() != null && currentNode != to){
			currentNode = currentNode.getParent();
			collapsePath(new TreePath(currentNode.getPath()));
		}
	}
	
	@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) {
					DiagramModelTreeNode treeNode = (DiagramModelTreeNode)getLastSelectedPathComponent();
					/* look if we've got a sibling node after (we are not at the bottom) */
					DiagramModelTreeNode nextTreeNode = treeNode.getNextSibling(); 
					SoundEvent loop = null;
					if(nextTreeNode == null){
						DiagramModelTreeNode parent = treeNode.getParent();
						if(parent == null) /* root node, just stay there */
							nextTreeNode = treeNode;
						else /* loop = go to first child of own parent */
							nextTreeNode = (DiagramModelTreeNode)parent.getFirstChild();
						loop = SoundEvent.LIST_BOTTOM_REACHED;
					}
					treeSelectionListenerGateOpen = false;
					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);
						}
					});
					treeSelectionListenerGateOpen = true;
					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) {
					DiagramModelTreeNode treeNode = (DiagramModelTreeNode)getLastSelectedPathComponent();
					DiagramModelTreeNode previousTreeNode = treeNode.getPreviousSibling();
					SoundEvent loop = null;
					if(previousTreeNode == null){
						DiagramModelTreeNode parent = treeNode.getParent();
						if(parent == null) /* root node */
							previousTreeNode = treeNode;
						else 
							previousTreeNode = (DiagramModelTreeNode)parent.getLastChild();
						loop = SoundEvent.LIST_TOP_REACHED;
					}
					treeSelectionListenerGateOpen = false;
					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);
						}
					});
					treeSelectionListenerGateOpen = true;
					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();
				   DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent();
				   if(treeNode.isLeaf()){
					   notifyBorderReached(treeNode);
					   InteractionLog.log(INTERACTIONLOG_SOURCE,"move right","border reached");
				   }
				   else{
					   treeSelectionListenerGateOpen = false;
					   expandPath(path);
					   setSelectionPath(new TreePath(((DiagramModelTreeNode)treeNode.getFirstChild()).getPath()));
					   final String currentPathSpeech = currentPathSpeech();
					   SoundFactory.getInstance().play(SoundEvent.TREE_NODE_EXPAND,new PlayerListener(){
						@Override
						public void playEnded() {
							NarratorFactory.getInstance().speak(currentPathSpeech);
						}
					   });
					   treeSelectionListenerGateOpen = true;
					   InteractionLog.log(INTERACTIONLOG_SOURCE,"move right",((DiagramModelTreeNode)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();
				   DiagramModelTreeNode treeNode = (DiagramModelTreeNode)path.getLastPathComponent();
				   DiagramModelTreeNode parent = treeNode.getParent();
				   if(parent == null){/* root node */
					   notifyBorderReached(treeNode);
					   InteractionLog.log(INTERACTIONLOG_SOURCE,"move left","border reached");
				   }
				   else{
					   treeSelectionListenerGateOpen = false;
					   TreePath newPath = new TreePath(((DiagramModelTreeNode)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",((DiagramModelTreeNode)parent).toString());
					   treeSelectionListenerGateOpen = true;
				   }
			   }
		   });
		   
		   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();
				   DiagramModelTreeNode treeNode = (DiagramModelTreeNode)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);
					   }
					   else{
						   if(!diagram.getModelUpdater().getLock(node, Lock.MUST_EXIST)){
							   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(DiagramModelTreeNode 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(DiagramModelTreeNode 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 boolean treeSelectionListenerGateOpen;
	private static final char SELECTED_NODE_MARK_BEGIN = '<';
	private static final char SELECTED_NODE_MARK_END = '>';
	private static final String INTERACTIONLOG_SOURCE = "TREE";
	static enum JumpTo {REFERENCE, ROOT, TYPE, SELECTED_TYPE, BOOKMARK}
	
	/* 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--){
					DiagramModelTreeNode onPathTreeNode = (DiagramModelTreeNode)pathArray[i];
					if(onPathTreeNode.isNodeRelated(root)){// if can reach the root from here a.k.a. the node is still part of the tree 
						super.treeStructureChanged(e);
						setSelectionPath(new TreePath(onPathTreeNode.getPath()));
						break;
					}else{
						/* check sibling nodes*/
						DefaultMutableTreeNode parent = (DiagramModelTreeNode)pathArray[i-1];
						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();
			DiagramModelTreeNode removedTreeNode = (DiagramModelTreeNode)e.getChildren()[0];
			boolean isInSelectionPath = false;
			for(Object t : getSelectionPath().getPath()){
				if(removedTreeNode == t){
					isInSelectionPath = true;
					break;
				}
			}
			DiagramModelTreeNode parentTreeNode = (DiagramModelTreeNode)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(((DiagramModelTreeNode)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--){
							DiagramModelTreeNode itr = (DiagramModelTreeNode)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 */
			DiagramModelTreeNode removedNode = (DiagramModelTreeNode)e.getChildren()[0];
			selectedNodes.remove(removedNode);
		}		
	}
}