fiore@0: /*
fiore@0: CCmI Diagram Editor for Android
fiore@0:
fiore@0: Copyright (C) 2012 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
fiore@0:
fiore@0: This program is free software: you can redistribute it and/or modify
fiore@0: it under the terms of the GNU General Public License as published by
fiore@0: the Free Software Foundation, either version 3 of the License, or
fiore@0: (at your option) any later version.
fiore@0:
fiore@0: This program is distributed in the hope that it will be useful,
fiore@0: but WITHOUT ANY WARRANTY; without even the implied warranty of
fiore@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
fiore@0: GNU General Public License for more details.
fiore@0:
fiore@0: You should have received a copy of the GNU General Public License
fiore@0: along with this program. If not, see .
fiore@0: */
fiore@0: package uk.ac.qmul.eecs.ccmi.activities;
fiore@0:
fiore@0: import java.util.ArrayList;
fiore@0: import java.util.LinkedHashMap;
fiore@0: import java.util.LinkedHashSet;
fiore@0: import java.util.LinkedList;
fiore@0: import java.util.List;
fiore@0: import java.util.Map;
fiore@0: import java.util.Set;
fiore@0:
fiore@0: import uk.ac.qmul.eecs.ccmi.accessibility.AccessibilityService;
fiore@0: import uk.ac.qmul.eecs.ccmi.accessibility.AccessibilityService.SoundEvent;
fiore@0: import uk.ac.qmul.eecs.ccmi.accessibility.AccessibleCheckbox;
fiore@0: import uk.ac.qmul.eecs.ccmi.accessibility.AccessibleDialogBuilder;
fiore@1: import uk.ac.qmul.eecs.ccmi.utilities.ILogger;
fiore@0: import uk.ac.qmul.eecs.ccmi.utilities.Stack;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.Diagram;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.DiagramUpdater;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.Edge;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.EdgeNode;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.EdgeType;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.HierarchyItem;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.LocalDiagramUpdater;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.Modifier;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.Node;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.NodeProperty;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.NodePropertyType;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.NodeType;
fiore@0: import uk.ac.qmul.eecs.ccmi.xmlparser.PropertyValue;
fiore@0: import android.support.v4.app.DialogFragment;
fiore@0: import android.view.View;
fiore@0: import android.widget.AdapterView;
fiore@0: import android.widget.EditText;
fiore@0: import android.widget.Spinner;
fiore@0:
fiore@0: /**
fiore@0: * Provides a navigation of the model (the diagram) based on the tree hierarchy of the
fiore@0: * CCmI Diagram Editor
fiore@0: *
fiore@0: */
fiore@0: class TreeNavigation {
fiore@0:
fiore@0: private static final String RENAME_DIALOG_TAG = "Rename ";
fiore@0: private static final String EDIT_NODE_DIALOG_TAG = "Edit Node Dialog";
fiore@0: private static final String EDIT_EDGE_DIALOG_TAG = "Edit Edge Dialog";
fiore@0: private static final String EDIT_NODE_REF_DIALOG_TAG = "Edit Node Reference Dialog";
fiore@0: private static final String EDIT_LABEL_DIALOG_TAG = "Edit Label";
fiore@0: private static final String EDIT_ARROWHEAD_DIALOG_TAG = "Edit Arrow Head Dialog";
fiore@0: private static final String CONFIRMATION_DIALOG_TAG = "Are you sure you want to delete";
fiore@0: private static final String ADD_PROPERTY_DIALOG_TAG = "Add Property Dialog";
fiore@0: private static final String EDIT_PROPERTY_DIALOG_TAG = "Edit Property Dialog";
fiore@0: private static final String EDIT_MODIFIERS_DIALOG_TAG = "Edit Modifiers Dialog";
fiore@0:
fiore@0: private Diagram diagram;
fiore@0: private DiagramUpdater diagramUpdater;
fiore@0: private Stack path;
fiore@0:
fiore@0: private List selectedNodes = new ArrayList(4);
fiore@0: /* cached list is filled up when next() and previous() and used to undo the navigation
fiore@0: * if the list created is empty. Once created, then it's returned by getCurrentList() */
fiore@0: private String[] cachedList;
fiore@0: private Stack cachedHeaderTexts;
fiore@0:
fiore@0: /* controller for edit actions with access to the current status of the navigation */
fiore@0: private Controller controller;
fiore@0: private Updateable updateable;
fiore@0:
fiore@0: /* the level currently displayed. Level item's toString() is on the header *
fiore@0: * text and its children are displayed in the currentList */
fiore@0: private final static int PATH_MAX_LEN = 5;
fiore@0: public final static int DIAGRAM_LEVEL = 0;
fiore@0: public final static int TYPE_LEVEL = 1;
fiore@0: public final static int ITEM_LEVEL = 2;
fiore@0: public final static int REFERENCE_AND_PROPERTY_TYPE_LEVEL = 3;
fiore@0: public final static int PROPERTY_AND_EDGE_REFERENCE_LEVEL = 4; // neved used
fiore@0:
fiore@0: public TreeNavigation(Diagram diagram, AccessibleDialogBuilder dialogBuilder, Updateable updateable){
fiore@0: this.diagram = diagram;
fiore@0: this.updateable = updateable;
fiore@0: diagramUpdater = new LocalDiagramUpdater(diagram);
fiore@0: path = new Stack(PATH_MAX_LEN);
fiore@0: cachedHeaderTexts = new Stack(PATH_MAX_LEN);
fiore@0: /* tree path starts with top node */
fiore@0: path.push(diagram);
fiore@0: cachedHeaderTexts.push(diagram.toString());
fiore@0: cachedList = buildCurrentChildList();
fiore@0: controller = new Controller(dialogBuilder);
fiore@0: }
fiore@0:
fiore@0: public String[] getCurrentChildList(){
fiore@0: return cachedList;
fiore@0: }
fiore@0:
fiore@0: public String getCurrentPath(){
fiore@0: StringBuilder builder = new StringBuilder();
fiore@0: for(HierarchyItem item : path){
fiore@0: builder.append('/').append(item);
fiore@0: }
fiore@0: return builder.toString();
fiore@0: }
fiore@0:
fiore@0: /* builds the String list of items to display when getCurrentListIsCalled */
fiore@0: private String[] buildCurrentChildList(){
fiore@0: String[] currentList = null;
fiore@0: switch(path.level()){
fiore@0: case DIAGRAM_LEVEL :{
fiore@0: List nodeTypes = diagram.getPrototypes().getNodeTypes();
fiore@0: List edgeTypes = diagram.getPrototypes().getEdgeTypes();
fiore@0: int numNodeTypes = nodeTypes.size();
fiore@0: int numEdgeTypes = edgeTypes.size();
fiore@0: currentList = new String[numNodeTypes+numEdgeTypes];
fiore@0:
fiore@0: /* build the string for each node type */
fiore@0: StringBuilder builder = new StringBuilder();
fiore@0: for(int i=0; i items = new LinkedList();
fiore@0: if(path.current() instanceof NodeType){
fiore@0: for(Node cNode : diagram.getComponents().getNodes()){
fiore@0: if(path.current().toString().equals(cNode.getType()))
fiore@0: if(selectedNodes.contains(cNode))
fiore@0: items.add("<"+cNode+">"); //
fiore@0: else
fiore@0: items.add(cNode.getName());
fiore@0: }
fiore@0: }else{
fiore@0: for(Edge cEdge : diagram.getComponents().getEdges()){
fiore@0: if(path.current().toString().equals(cEdge.getType()))
fiore@0: items.add(cEdge.getName());
fiore@0: }
fiore@0: }
fiore@0: currentList = items.toArray(new String[items.size()]);
fiore@0: break;
fiore@0: }
fiore@0: case ITEM_LEVEL : {
fiore@0: Map edgeTypes = new LinkedHashMap();
fiore@0: if(path.current() instanceof Node){
fiore@0: /* node displays the list of edge types connected to *
fiore@0: * itself + the list of property types defined for itself */
fiore@0: Node node = (Node)path.current();
fiore@0: /* add the edge types of the edges the node is attached to */
fiore@0: for(Edge cEdge : diagram.getComponents().getEdges()){
fiore@0: for(EdgeNode attachedNode : cEdge.getAttachedNodes()){
fiore@0: if(attachedNode.getId() == node.getId()){
fiore@0: if(!edgeTypes.containsKey(cEdge.getType())){
fiore@0: edgeTypes.put(cEdge.getType(),1);
fiore@0: }else{
fiore@0: edgeTypes.put(cEdge.getType(),edgeTypes.get(cEdge.getType())+1);
fiore@0: }
fiore@0: break;
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0:
fiore@0: /* build the array to return : first part is the edge types. second part property types /
fiore@0: /* now the property types this node can have */
fiore@0: currentList = new String[edgeTypes.size()+node.getProperties().size()];
fiore@0: int i = 0;
fiore@0: StringBuilder builder = new StringBuilder();
fiore@0: for(Map.Entry entry : edgeTypes.entrySet()){
fiore@0: builder.setLength(0);
fiore@0: currentList[i++] = builder.append(entry.getKey())
fiore@0: .append(' ')
fiore@0: .append('(')
fiore@0: .append(entry.getValue())
fiore@0: .append(')').toString();
fiore@0: }
fiore@0:
fiore@0: for(int j=i;j modifierIndexes = property.getValues().get(i).getModifiersIndexes();
fiore@0: for(NodePropertyType propertyType : nodeType.getPropretyTypes()){
fiore@0: if(propertyType.getType().equals(property.getType())){
fiore@0: ArrayList modifiers = propertyType.getModifiers();
fiore@0: for(Integer j : modifierIndexes){
fiore@0: builder.append(modifiers.get(j).getType()).append(' ');
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0: currentList[i] = builder.append(property.getValues().get(i).getValue()).toString();
fiore@0: }
fiore@0:
fiore@0: // NodeProperty propertyType = (NodeProperty)path.current();
fiore@0: // Node node = (Node)path.get(ITEM_LEVEL);
fiore@0: // for(NodeProperty property : node.getProperties()){
fiore@0: // if(property.getType().equals(propertyType.getType())){
fiore@0: // currentList = new String[property.getValues().size()];
fiore@0: // for(int i=0; i currentListItems = new LinkedList();
fiore@0: int nodeID = ((Node)path.get(ITEM_LEVEL)).getId();
fiore@0: StringBuilder builder = new StringBuilder();
fiore@0: for(Edge edge : diagram.getComponents().getEdges()){
fiore@0: if(edge.getType().equals(edgeType.getType())){
fiore@0: for(EdgeNode attachedNode : edge.getAttachedNodes()){
fiore@0: if(attachedNode.getId() == nodeID){
fiore@0: currentListItems.add(makeEdgeReferenceString(edge,nodeID,builder));
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0: currentList = currentListItems.toArray(new String[currentListItems.size()]);
fiore@0: }
fiore@0: break;
fiore@0: }
fiore@0: default : new IllegalStateException("Wrong path level: "+path.level());
fiore@0: }
fiore@0: return currentList;
fiore@0: }
fiore@0:
fiore@0: /**
fiore@0: * Returns the label of the parent of the list currently visualized
fiore@0: *
fiore@0: * @return the label of the parent
fiore@0: */
fiore@0: public String getCurrentItemName(){
fiore@0: String text = cachedHeaderTexts.current();
fiore@0: if(text != null)
fiore@0: return text;
fiore@0: else
fiore@0: return path.current().toString();
fiore@0: }
fiore@0:
fiore@0: /**
fiore@0: * Returns the level of the navigation at the moment the method is called.
fiore@0: *
fiore@0: * @return an {@code int} representing the current level. Possible values are
fiore@0: * listed as {@code static} variables of this class.
fiore@0: */
fiore@0: public int getCurrentLevel(){
fiore@0: return path.level();
fiore@0: }
fiore@0:
fiore@0: /**
fiore@0: * Makes a child of the current {@code HierarchyItem} the current one.
fiore@0: *
fiore@0: * @param child the index of the child becoming the current
fiore@0: * @return {@code true} if the current position has changed after the call
fiore@0: **/
fiore@0: public boolean goNext(int child){
fiore@0: if(child < 0)
fiore@0: throw new IllegalArgumentException("Wrong child index: "+child);
fiore@0:
fiore@0: HierarchyItem hierarchyItem = findNext(child);
fiore@0: if(hierarchyItem == null)
fiore@0: return false;
fiore@0:
fiore@0: path.push(hierarchyItem);
fiore@0: /* saves the selected list item to display as a title on the next call to getCurrentHeaderText() */
fiore@0: cachedHeaderTexts.push(cachedList[child]);
fiore@0: /* make the new list to return for next calls to getCurrentList() */
fiore@0: cachedList = buildCurrentChildList();
fiore@0: /* the selected hierarchy node has no children. go back and return false */
fiore@0: if(cachedList.length == 0){
fiore@0: goPrevious();
fiore@0: return false;
fiore@0: }
fiore@0: updateable.update();
fiore@0: return true;
fiore@0: }
fiore@0:
fiore@0: private HierarchyItem findNext(int child){
fiore@0: switch(path.level()){
fiore@0: case DIAGRAM_LEVEL :{
fiore@0: List nodeTypes = diagram.getPrototypes().getNodeTypes();
fiore@0: List edgeTypes = diagram.getPrototypes().getEdgeTypes();
fiore@0: int numNodeTypes = nodeTypes.size();
fiore@0: int numEdgeTypes = edgeTypes.size();
fiore@0: if(child >= numNodeTypes+numEdgeTypes)
fiore@0: throw new IllegalArgumentException("Wrong child index: "+child);
fiore@0:
fiore@0: if(child < numNodeTypes){
fiore@0: return nodeTypes.get(child);
fiore@0: }else{
fiore@0: return edgeTypes.get(child-numNodeTypes);
fiore@0: }
fiore@0: }
fiore@0: case TYPE_LEVEL : {
fiore@0: int numTypeItems = 0;// the number of nodes or edges found of the current type
fiore@0: if(path.current() instanceof NodeType){
fiore@0: /* find the child-th node of the current type */
fiore@0: for(Node node : diagram.getComponents().getNodes()){
fiore@0: if(path.current().toString().equals(node.getType())){
fiore@0: if(numTypeItems == child){
fiore@0: return node;
fiore@0: }else{
fiore@0: numTypeItems++;
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0: }else{ // EdgeType
fiore@0: for(Edge edge : diagram.getComponents().getEdges()){
fiore@0: /* find the child-th edge of the current type */
fiore@0: if(path.current().toString().equals(edge.getType())){
fiore@0: if(numTypeItems == child){
fiore@0: return edge;
fiore@0: }else{
fiore@0: numTypeItems++;
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0: break;
fiore@0: }
fiore@0: case ITEM_LEVEL : {
fiore@0: /* we can go further only for nodes */
fiore@0: if(path.current() instanceof Node){
fiore@0: Node node = (Node)path.current();
fiore@0: /* check if user clicked on an edge, this node is attached to */
fiore@0:
fiore@0: Set cNodeEdgeTypes = new LinkedHashSet();
fiore@0: /* for each edge component */
fiore@0: for(Edge edge : diagram.getComponents().getEdges()){
fiore@0: /* for each attached node to this edge */
fiore@0: for(EdgeNode attachedNode : edge.getAttachedNodes()){
fiore@0: /* if it's this node we're looking for (cNode) */
fiore@0: if(attachedNode.getId() == node.getId()){
fiore@0: cNodeEdgeTypes.add(edge.getType());
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0:
fiore@0: if(child < cNodeEdgeTypes.size()){
fiore@0: String selectedType = cNodeEdgeTypes.toArray(new String[cNodeEdgeTypes.size()])[child];
fiore@0: for(EdgeType edge : diagram.getPrototypes().getEdgeTypes()){
fiore@0: if(edge.getType().equals(selectedType)){
fiore@0: return edge;
fiore@0: }
fiore@0: }
fiore@0: }else{// user selected a NodePropertyType put a NodeProperty in the path
fiore@0: return node.getProperties().get(child-cNodeEdgeTypes.size());
fiore@0: }
fiore@0: break;
fiore@0: }
fiore@0: }
fiore@0: default : return null; // don't go further than REFERENCE_AND_PROPERTY_LEVEL
fiore@0: }
fiore@0: return null;
fiore@0: }
fiore@0:
fiore@0: /**
fiore@0: * Makes the father of the current {@code HierarchyItem} the current one.
fiore@0: * @return @code true} if the current position has changed after the call
fiore@0: */
fiore@0: public boolean goPrevious(){
fiore@0: if(path.level() == DIAGRAM_LEVEL) // level = DIAGRAM_LEVEL
fiore@0: return false;
fiore@0: path.pop();
fiore@0: cachedList = buildCurrentChildList();
fiore@0: cachedHeaderTexts.pop();
fiore@0: return true;
fiore@0: }
fiore@0:
fiore@0: /**
fiore@0: * Returns a controller, that can be used to
fiore@0: * change the model (the diagram) in a way coherent to the tree navigation
fiore@0: *
fiore@0: * @return a controller connected to this navigation
fiore@0: */
fiore@0: public Controller getController(){
fiore@0: return controller;
fiore@0: }
fiore@0:
fiore@0: /* Counts the number of nodes or edges of a given type */
fiore@0: private int count(HierarchyItem hItem){
fiore@0: int count = 0;
fiore@0: if(hItem instanceof NodeType){
fiore@0: NodeType node = (NodeType)hItem;
fiore@0: for(Node cNode : diagram.getComponents().getNodes())
fiore@0: if(cNode.getType().equals(node.getType()))
fiore@0: count++;
fiore@0: }else if(hItem instanceof EdgeType){
fiore@0: EdgeType edge = (EdgeType)hItem;
fiore@0: for(Edge cEdge : diagram.getComponents().getEdges())
fiore@0: if(cEdge.getType().equals(edge.getType()))
fiore@0: count++;
fiore@0: }
fiore@0: return count;
fiore@0: }
fiore@0:
fiore@0: /* Creates a string for the node reference of edge related to the node N equal to ID nodeID.
fiore@0: * The string looks like "to N1, N2 and N3 vie E", where N1, N2 and N3 are the nodes connected
fiore@0: * to N via E ( they can be more than one in case of a multiple ended edge ).
fiore@0: */
fiore@0: private String makeEdgeReferenceString(Edge edge, int nodeID, StringBuilder builder) {
fiore@0: builder.setLength(0);
fiore@0: builder.append("to ");
fiore@0: /* attach the name of the nodes interleaved by " and " */
fiore@0: for(int i=0; i parent, View view,
fiore@0: int position, long id, Updateable u) {
fiore@0: switch(getCurrentLevel()){
fiore@0: case TreeNavigation.DIAGRAM_LEVEL : {
fiore@0: List nodeTypes = diagram.getPrototypes().getNodeTypes();
fiore@0: if(position < nodeTypes.size()){ // user clicked on a node
fiore@0: clickedItem = nodeTypes.get(position);
fiore@0: /* build the properties for node constructor using node property types */
fiore@0: NodeType nodeType = nodeTypes.get(position);
fiore@0: ArrayList properties = new ArrayList(nodeType.getPropretyTypes().size());
fiore@0: for(NodePropertyType pType : nodeType.getPropretyTypes()){
fiore@0: properties.add(new NodeProperty(pType.getType()));
fiore@0: }
fiore@0: /* construct the node and add it to the diagram */
fiore@0: Node node = new Node(((NodeType)clickedItem).getType(),properties);
fiore@0: diagramUpdater.addNode(node);
fiore@1: dialogBuilder.getAccessibilityService().playSound(SoundEvent.V_OK);
fiore@0: dialogBuilder.getAccessibilityService().speak("Node "+ node +" created");
fiore@1: ILogger.log("Node "+ node +" created");
fiore@0: }else{ // user clicked on edge
fiore@0: List edgeTypes = diagram.getPrototypes().getEdgeTypes();
fiore@0: clickedItem = edgeTypes.get(position - nodeTypes.size());
fiore@0: /* respect min and max attached nodes */
fiore@0: if(selectedNodes.size() < ((EdgeType)clickedItem).getMinAttachedNodes()){
fiore@1: dialogBuilder.getAccessibilityService().playSound(SoundEvent.V_ERROR);
fiore@0: dialogBuilder.getAccessibilityService().speak("you must select at least "+
fiore@0: ((EdgeType)clickedItem).getMinAttachedNodes()+" nodes");
fiore@1: ILogger.logError("selected nodes < "+((EdgeType)clickedItem).getMinAttachedNodes());
fiore@0: return true;
fiore@0: }
fiore@0: if(selectedNodes.size() > ((EdgeType)clickedItem).getMaxAttachedNodes()){
fiore@1: dialogBuilder.getAccessibilityService().playSound(SoundEvent.V_ERROR);
fiore@0: dialogBuilder.getAccessibilityService().speak("you must select at most "+
fiore@0: ((EdgeType)clickedItem).getMaxAttachedNodes()+" nodes");
fiore@1: ILogger.logError("selected nodes > "+((EdgeType)clickedItem).getMaxAttachedNodes());
fiore@0: return true;
fiore@0: }
fiore@0:
fiore@0: /* create the edge and add the edge nodes */
fiore@0: Edge edge = new Edge(((EdgeType)clickedItem).getType());
fiore@0: edge.getAttachedNodes().clear();
fiore@0: /* create all the node references (EdgeNode) for this edge */
fiore@0: for(Node n : diagram.getComponents().getNodes()){
fiore@0: if(selectedNodes.contains(n))
fiore@0: edge.getAttachedNodes().add(new EdgeNode(n.getId()));
fiore@0: }
fiore@0: diagramUpdater.addEdge(edge);
fiore@1: dialogBuilder.getAccessibilityService().playSound(SoundEvent.V_OK);
fiore@0: StringBuilder builder = new StringBuilder();
fiore@0: builder.append(edge).append(" created between ");
fiore@0: for(int i=0; i modifiers = new ArrayList(checks.length);
fiore@1: StringBuilder modifiersString = new StringBuilder();
fiore@0: for(int i=0; i