diff java/src/uk/ac/qmul/eecs/ccmi/gui/CCmIPopupMenu.java @ 3:9e67171477bc

PHANTOM Omni Heptic device release
author Fiore Martin <fiore@eecs.qmul.ac.uk>
date Wed, 25 Apr 2012 17:09:09 +0100
parents
children d66dd5880081
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/CCmIPopupMenu.java	Wed Apr 25 17:09:09 2012 +0100
@@ -0,0 +1,442 @@
+/*  
+ CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
+  
+ Copyright (C) 2011  Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+package uk.ac.qmul.eecs.ccmi.gui;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPopupMenu;
+
+import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement;
+import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties;
+import uk.ac.qmul.eecs.ccmi.network.AwarenessMessage;
+import uk.ac.qmul.eecs.ccmi.network.Command;
+import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource;
+import uk.ac.qmul.eecs.ccmi.utils.InteractionLog;
+
+/**
+ * This class provides the two menus to handle nodes and edges on the visual graph. This class 
+ * provides an abstract implementation common to both the node and edge menus. The specific 
+ * implementations are internal static classes, inheriting from this class.
+ *
+ */
+@SuppressWarnings("serial")
+public abstract class CCmIPopupMenu extends JPopupMenu {
+	/**
+	 * This constructor is called by subclasses constructor. 
+	 * 
+	 * @param reference the element this menu refers to (it popped up by right-clicking on it)
+	 * @param parentComponent the component where the menu is going to be displayed
+	 * @param modelUpdater the model updater to make changed to {@code reference}
+	 * @param selectedElements other elements eventually selected on the graph, which are going
+	 * to undergo the same changes as {@code reference}, being selected together with it.  
+	 */
+	protected CCmIPopupMenu(DiagramElement reference,
+			Component parentComponent, DiagramModelUpdater modelUpdater,
+			Set<DiagramElement> selectedElements) {
+		super();
+		this.modelUpdater = modelUpdater;
+		this.parentComponent = parentComponent;
+		this.reference = reference;
+		this.selectedElements = selectedElements;
+	}
+
+	/**
+	 * Returns the the element this menu refers to.
+	 * @return the element this menu refers to.
+	 */
+	public DiagramElement getElement(){
+		return reference;
+	}
+	
+	/**
+	 * Add the a menu item to this menu. A menu item, once clicked on, will prompt the user for a new name
+	 * for the referee element and will execute the update through the modelUpdater passed as argument 
+	 * to the constructor.   
+	 */
+	protected void addNameMenuItem() {
+		/* add set name menu item */
+		JMenuItem setNameMenuItem = new JMenuItem(
+				resources.getString("menu.set_name"));
+		setNameMenuItem.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				String type = (reference instanceof Node) ? "node" : "edge";
+				if (!modelUpdater.getLock(reference, Lock.NAME,
+						new DiagramEventActionSource(DiagramEventSource.GRPH,
+								Command.Name.SET_NODE_NAME, reference.getId(),reference.getName()))) {
+					iLog("Could not get the lock on " + type + " for renaming",
+							DiagramElement.toLogString(reference));
+					JOptionPane.showMessageDialog(parentComponent,
+							MessageFormat.format(resources
+									.getString("dialog.lock_failure.name"),
+									type));
+					return;
+				}
+				iLog("open rename " + type + " dialog",
+						DiagramElement.toLogString(reference));
+				String name = JOptionPane.showInputDialog(parentComponent,
+						MessageFormat.format(
+								resources.getString("dialog.input.name"),
+								reference.getName()), reference.getName());
+				if (name == null)
+					iLog("cancel rename " + type + " dialog",
+							DiagramElement.toLogString(reference));
+				else
+					/* node has been locked at selection time */
+					modelUpdater.setName(reference, name.trim(),
+							DiagramEventSource.GRPH);
+				modelUpdater.yieldLock(reference, Lock.NAME,
+						new DiagramEventActionSource(DiagramEventSource.GRPH,
+								Command.Name.SET_NODE_NAME, reference.getId(),reference.getName()));
+			}
+
+		});
+		add(setNameMenuItem);
+	}
+
+	/**
+	 * Add the a delete item to this menu. A menu item, once clicked on, will prompt the user for a confirmation
+	 * for the deletion of referee element and will execute the update through the modelUpdater passed as argument 
+	 * to the constructor.
+	 */
+	protected void addDeleteMenuItem() {
+		JMenuItem deleteMenuItem = new JMenuItem(resources.getString("menu.delete"));
+		deleteMenuItem.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent evt) {
+				/* create a new Set to maintain iterator consistency as elementTakenOut will change selectedItems  */
+				List<DiagramElement> workList = new ArrayList<DiagramElement>(selectedElements); 
+				/* right click on an element with no selection involved */
+				if(workList.isEmpty()){
+					workList.add(reference);
+				/* right click on an element with other elements selected, thus we ignore the *
+				 * currently selected elements and try to delete only the right clicked one.  */
+				}else if(!workList.contains(reference)){
+					workList.clear();
+					workList.add(reference);
+				}else{
+					/* If the right clicked element selected together with other elements, try to         * 
+					 * delete them all. First delete all edges and then all nodes to keep consistency.    *
+					 * We are deleting a bunch of objects and if a node is deleted before an edge         *  
+					 * attached  to it, then an exception will be triggered at the moment of edge         *
+					 * deletion because the edge will be already deleted as a result of the node deletion */
+					Collections.sort(workList, new Comparator<DiagramElement>(){
+						@Override
+						public int compare(DiagramElement e1,DiagramElement e2) {
+							boolean e1isEdge = e1 instanceof Edge;
+							boolean e2isEdge = e2 instanceof Edge;
+							if(e1isEdge && !e2isEdge){
+								return -1;
+							}
+							if(!e1isEdge && e2isEdge){
+								return 1;
+							}
+							return 0;
+						}
+					});
+				}
+					
+				List<DiagramElement>alreadyLockedElements = new ArrayList<DiagramElement>(workList.size());
+				/* check which, of the selected elements, can be deleted and which ones are currently held by     *
+				 * other clients. If an element is locked it's removed from the list and put into a separated set */
+				for(Iterator<DiagramElement> itr=workList.iterator(); itr.hasNext();){
+					DiagramElement  selected = itr.next();
+					boolean isNode = selected instanceof Node;
+					if(!modelUpdater.getLock(selected, 
+							Lock.DELETE, 
+							new DiagramEventActionSource(
+									DiagramEventSource.GRPH, 
+									isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE,
+									selected.getId(),selected.getName()))){
+						itr.remove();
+						alreadyLockedElements.add(selected);
+					}
+				}
+				
+				/* all the elements are locked by other clients */
+				if(workList.isEmpty()){
+					iLog("Could not get lock on any selected element for deletion","");
+					JOptionPane.showMessageDialog(
+							JOptionPane.getFrameForComponent(parentComponent), 
+							alreadyLockedElements.size() == 1 ? // singular vs plural 
+									resources.getString("dialog.lock_failure.delete") :
+										resources.getString("dialog.lock_failure.deletes"));
+					return;
+				}
+
+				String warning = "";
+				if(!alreadyLockedElements.isEmpty()){
+					StringBuilder builder = new StringBuilder(resources.getString("dialog.lock_failure.deletes_warning"));
+					for(DiagramElement alreadyLocked : alreadyLockedElements)
+						builder.append(alreadyLocked.getName()).append(' ');
+					warning = builder.append('\n').toString();
+					iLog("Could not get lock on some selected element for deletion",warning);
+				}
+
+				iLog("open delete dialog",warning);
+				int answer = JOptionPane.showConfirmDialog(
+						JOptionPane.getFrameForComponent(parentComponent),
+						warning+resources.getString("dialog.confirm.deletions"), 
+						resources.getString("dialog.confirm.title"),
+						SpeechOptionPane.YES_NO_OPTION);
+				if(answer == JOptionPane.YES_OPTION){
+					/* the user chose to delete the elements, proceed (locks       *
+					 * will be automatically removed upon deletion by the server ) */
+					for(DiagramElement selected : workList){
+						modelUpdater.takeOutFromCollection(selected,DiagramEventSource.GRPH);
+						modelUpdater.sendAwarenessMessage(
+								   AwarenessMessage.Name.STOP_A,
+								   new DiagramEventActionSource(DiagramEventSource.TREE,
+										   (selected instanceof Node) ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE,
+										   selected.getId(),selected.getName())
+								   );
+					}
+				}else{
+					/* the user chose not to delete the elements, release the acquired locks */
+					for(DiagramElement selected : workList){
+						/* if it's a node all its attached edges were locked as well */
+						/*if(selected instanceof Node){ DONE IN THE SERVER
+						Node n = (Node)selected;
+						for(int i=0; i<n.getEdgesNum();i++){
+							modelUpdater.yieldLock(n.getEdgeAt(i), Lock.DELETE);
+						}}*/
+						boolean isNode = selected instanceof Node;
+						modelUpdater.yieldLock(selected, 
+								Lock.DELETE,
+								new DiagramEventActionSource(
+										DiagramEventSource.GRPH, 
+										isNode ? Command.Name.REMOVE_NODE : Command.Name.REMOVE_EDGE,
+												selected.getId(),selected.getName()));
+					}
+					iLog("cancel delete node dialog","");
+				}
+			}
+		});
+		add(deleteMenuItem);
+	}
+
+	/**
+	 * Performs the log in the InteractionLog.
+	 * @param action the action to log.
+	 * @param args additional arguments to add to the log.
+	 * 
+	 * @see uk.ac.qmul.eecs.ccmi.utils#InteractionLog
+	 */
+	protected void iLog(String action, String args) {
+		InteractionLog.log("GRAPH", action, args);
+	}
+	
+	/**
+	 * 
+	 * A popup menu to perform changes (e.g. delete, rename etc.) to a node from the visual graph. 
+	 *
+	 */
+	public static class NodePopupMenu extends CCmIPopupMenu {
+		/**
+		 * 
+		 * @param node the node this menu refers to.
+		 * @param parentComponent the component where the menu is going to be displayed.
+		 * @param modelUpdater the model updater used to make changed to {@code node}.
+		 * @param selectedElements other elements eventually selected on the graph, which are going
+		 * to undergo the same changes as {@code node}, being selected together with it.  
+		 */
+		NodePopupMenu(Node node, Component parentComponent,
+				DiagramModelUpdater modelUpdater,
+				Set<DiagramElement> selectedElements) {
+			super(node, parentComponent, modelUpdater, selectedElements);
+			addNameMenuItem();
+			addPropertyMenuItem();
+			addDeleteMenuItem();
+		}
+
+		private void addPropertyMenuItem() {
+			final Node nodeRef = (Node) reference;
+			/* if the node has no properties defined, then don't add the menu item */
+			if(nodeRef.getProperties().isNull())
+				return;
+			/* add set property menu item*/
+			JMenuItem setPropertiesMenuItem = new JMenuItem(resources.getString("menu.set_properties"));
+			setPropertiesMenuItem.addActionListener(new ActionListener(){
+				@Override
+				public void actionPerformed(ActionEvent e) {
+					if(!modelUpdater.getLock(nodeRef, Lock.PROPERTIES, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName()))){
+						iLog("Could not get the lock on node for properties",DiagramElement.toLogString(nodeRef));
+						JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.properties"));
+						return;
+					}
+					iLog("open edit properties dialog",DiagramElement.toLogString(nodeRef));
+					NodeProperties properties = PropertyEditorDialog.showDialog(JOptionPane.getFrameForComponent(parentComponent),nodeRef.getPropertiesCopy()); 
+					if(properties == null){ // user clicked on cancel 
+						iLog("cancel edit properties dialog",DiagramElement.toLogString(nodeRef));
+						modelUpdater.yieldLock(nodeRef, Lock.PROPERTIES, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName()));
+						return;
+					}
+					if(!properties.isNull())
+						modelUpdater.setProperties(nodeRef,properties,DiagramEventSource.GRPH);
+					modelUpdater.yieldLock(nodeRef, Lock.PROPERTIES,new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_PROPERTIES,nodeRef.getId(),reference.getName()));
+				}
+			});
+			add(setPropertiesMenuItem);
+		}
+	}
+	
+	/**
+	 * A popup menu to perform changes (e.g. delete, rename etc.) to a edge from the visual graph. 
+	 */
+	public static class EdgePopupMenu extends CCmIPopupMenu {
+		/**
+		 * Constructs an {@code EdgePopupMenu} to perform changes to an edge from the visual diagram. 
+		 * This constructor is normally called when the user clicks in the neighbourhood of a node 
+		 * connected to this edge. the menu will then include items to change an end label or 
+		 * an arrow head.  
+		 * @param edge the edge this menu refers to.
+		 * @param node one attached node some menu item will refer to.
+		 * @param parentComponent the component where the menu is going to be displayed.
+		 * @param modelUpdater the model updater used to make changed to {@code edge}.
+		 * @param selectedElements other elements eventually selected on the graph, which are going
+		 * to undergo the same changes as {@code edge}, being selected together with it. 
+		 */
+		public EdgePopupMenu( Edge edge, Node node, Component parentComponent, DiagramModelUpdater modelUpdater,
+				Set<DiagramElement> selectedElements){
+			super(edge,parentComponent,modelUpdater,selectedElements);
+			addNameMenuItem();
+			if(node != null){
+				nodeRef = node;
+				Object[] arrowHeads = new Object[edge.getAvailableEndDescriptions().length + 1];
+				for(int i=0;i<edge.getAvailableEndDescriptions().length;i++){
+					arrowHeads[i] = edge.getAvailableEndDescriptions()[i].toString();
+				}
+				arrowHeads[arrowHeads.length-1] = Edge.NO_ENDDESCRIPTION_STRING;
+				addEndMenuItems(arrowHeads);
+			}
+			addDeleteMenuItem();
+		}
+		
+		/**
+		 * Constructs an {@code EdgePopupMenu} to perform changes to an edge from the visual diagram. 
+		 * This constructor is normally called when the user clicks around the midpoint of the edge 
+		 * @param edge the edge this menu refers to.
+		 * @param parentComponent the component where the menu is going to be displayed.
+		 * @param modelUpdater the model updater used to make changed to {@code edge}.
+		 * @param selectedElements other elements eventually selected on the graph, which are going
+		 * to undergo the same changes as {@code edge}, being selected together with it. 
+		 */
+		public EdgePopupMenu( Edge edge, Component parentComponent, DiagramModelUpdater modelUpdater,
+				Set<DiagramElement> selectedElements){
+			this(edge,null,parentComponent,modelUpdater,selectedElements);
+		}
+		
+		private void addEndMenuItems(final Object[] arrowHeads){
+			final Edge edgeRef = (Edge)reference; 
+			/* Label menu item */
+			JMenuItem setLabelMenuItem = new JMenuItem(resources.getString("menu.set_label"));
+			setLabelMenuItem.addActionListener(new ActionListener(){
+				@Override
+				public void actionPerformed(ActionEvent evt) {
+					if(!modelUpdater.getLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDLABEL,edgeRef.getId(),reference.getName()))){
+						iLog("Could not get lock on edge for end label",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef));
+						JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.end"));
+						return;
+					}
+					iLog("open edge label dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef));
+					String text = JOptionPane.showInputDialog(parentComponent,resources.getString("dialog.input.label"));
+					if(text != null)
+						modelUpdater.setEndLabel(edgeRef,nodeRef,text,DiagramEventSource.GRPH);
+					else
+						iLog("cancel edge label dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef));
+					modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDLABEL,edgeRef.getId(),reference.getName()));
+				}
+			});
+			add(setLabelMenuItem);
+
+			if(arrowHeads.length > 1){
+				/* arrow head menu item */
+				JMenuItem selectArrowHeadMenuItem = new JMenuItem(resources.getString("menu.choose_arrow_head"));
+				selectArrowHeadMenuItem.addActionListener(new ActionListener(){
+					@Override
+					public void actionPerformed(ActionEvent e) {
+						if(!modelUpdater.getLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName()))){
+							iLog("Could not get lock on edge for arrow head",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef));
+							JOptionPane.showMessageDialog(parentComponent, resources.getString("dialog.lock_failure.end"));
+							return;
+						}
+						iLog("open select arrow head dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef));
+						String arrowHead = (String)JOptionPane.showInputDialog(
+								parentComponent, 
+								resources.getString("dialog.input.arrow"), 
+								resources.getString("dialog.input.arrow.title"), 
+								JOptionPane.PLAIN_MESSAGE, 
+								null, 
+								arrowHeads, 
+								arrowHeads);
+						if(arrowHead == null){
+							iLog("cancel select arrow head dialog",DiagramElement.toLogString(edgeRef)+" end node:"+DiagramElement.toLogString(nodeRef));
+							modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName()));
+							return;
+						}
+						for(int i=0; i<edgeRef.getAvailableEndDescriptions().length;i++){
+							if(edgeRef.getAvailableEndDescriptions()[i].toString().equals(arrowHead)){
+								modelUpdater.setEndDescription(edgeRef, nodeRef, i,DiagramEventSource.GRPH);
+								modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName()));
+								return;
+							}
+						}
+						/* the user selected the none menu item */
+						modelUpdater.setEndDescription(edgeRef,nodeRef, Edge.NO_END_DESCRIPTION_INDEX,DiagramEventSource.GRPH);
+						modelUpdater.yieldLock(edgeRef, Lock.EDGE_END, new DiagramEventActionSource(DiagramEventSource.GRPH,Command.Name.SET_ENDDESCRIPTION,edgeRef.getId(),reference.getName()));
+					}
+				});
+				add(selectArrowHeadMenuItem);
+			}
+		}
+		
+		private Node nodeRef;
+	}
+
+	/**
+	 * the model updater used to make changed to {@code reference}.
+	 */
+	protected DiagramModelUpdater modelUpdater;
+	/**
+	 * the component where the menu is going to be displayed.
+	 */
+	protected Component parentComponent;
+	/**
+	 * the element this menu refers to.
+	 */
+	protected DiagramElement reference;
+	/**
+	 * other elements eventually selected on the graph, which are going
+	 * to undergo the same changes as {@code reference}, being selected together with it.
+	 */
+	protected Set<DiagramElement> selectedElements;
+	private static ResourceBundle resources = ResourceBundle.getBundle(CCmIPopupMenu.class.getName());
+}