view java/src/uk/ac/qmul/eecs/ccmi/gui/CCmIPopupMenu.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.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());
}