view java/src/uk/ac/qmul/eecs/ccmi/gui/DiagramTree.java @ 8:ea7885bd9bff tip

fixed bug : render solid line as dotted/dashed when moving the stylus from dotted/dashed to solid
author ccmi-guest
date Thu, 03 Jul 2014 16:12:20 +0100
parents d66dd5880081
children
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.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);
		}		
	}
}