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