fiore@0: /* fiore@0: CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool fiore@0: fiore@0: Copyright (C) 2011 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: fiore@0: package uk.ac.qmul.eecs.ccmi.simpletemplate; fiore@0: fiore@0: import java.awt.Component; fiore@0: import java.awt.Dimension; fiore@0: import java.awt.FlowLayout; fiore@0: import java.awt.Frame; fiore@0: import java.awt.GridBagLayout; fiore@0: import java.awt.GridLayout; fiore@0: import java.awt.event.ActionEvent; fiore@0: import java.awt.event.KeyEvent; fiore@0: import java.io.IOException; fiore@0: import java.text.MessageFormat; fiore@0: import java.util.ArrayList; fiore@0: import java.util.Arrays; fiore@0: import java.util.Collection; fiore@0: import java.util.LinkedHashMap; fiore@0: import java.util.LinkedHashSet; fiore@0: import java.util.ResourceBundle; fiore@0: import java.util.Set; fiore@0: fiore@0: import javax.swing.AbstractAction; fiore@0: import javax.swing.DefaultComboBoxModel; fiore@0: import javax.swing.JCheckBox; fiore@0: import javax.swing.JComboBox; fiore@0: import javax.swing.JComponent; fiore@0: import javax.swing.JPanel; fiore@0: import javax.swing.JScrollPane; fiore@0: import javax.swing.JTextField; fiore@0: import javax.swing.KeyStroke; fiore@0: import javax.swing.SpinnerModel; fiore@0: fiore@0: import jwizardcomponent.FinishAction; fiore@0: import jwizardcomponent.JWizardComponents; fiore@0: import jwizardcomponent.JWizardPanel; fiore@0: import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties; fiore@0: import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers; fiore@0: import uk.ac.qmul.eecs.ccmi.gui.Diagram; fiore@0: import uk.ac.qmul.eecs.ccmi.gui.Edge; fiore@0: import uk.ac.qmul.eecs.ccmi.gui.LineStyle; fiore@0: import uk.ac.qmul.eecs.ccmi.gui.LoopComboBox; fiore@0: import uk.ac.qmul.eecs.ccmi.gui.LoopSpinnerNumberModel; fiore@0: import uk.ac.qmul.eecs.ccmi.gui.Node; fiore@0: import uk.ac.qmul.eecs.ccmi.gui.SpeechSummaryPane; fiore@0: import uk.ac.qmul.eecs.ccmi.simpletemplate.SimpleShapeNode.ShapeType; fiore@0: import uk.ac.qmul.eecs.ccmi.sound.SoundEvent; fiore@0: import uk.ac.qmul.eecs.ccmi.sound.SoundFactory; fiore@0: import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory; fiore@0: import uk.ac.qmul.eecs.ccmi.speech.SpeechUtilities; fiore@0: import uk.ac.qmul.eecs.ccmi.utils.GridBagUtilities; fiore@0: fiore@0: /** fiore@0: * fiore@0: * A Wizard-like sequence of screens prompted to the user to let they input (e.g. which shape a node will have or how many nodes an edge can connect at most) fiore@0: * how to build a template diagram. A template diagram is a prototype diagram fiore@0: * (containing prototype nodes and edges) which can later on be used for creating instances fiore@0: * of that type of diagram through clonation. The wizard is completely accessible via audio fiore@0: * as all the content and all focused components names are spoken out by the {@link Narrator} through a text to speech synthesizer. fiore@0: * fiore@0: */ fiore@0: public class Wizard { fiore@0: public Wizard(Frame frame, Collection existingDiagrams, Diagram diagramToEdit){ fiore@0: dialog = new SpeechWizardDialog(frame); fiore@0: resources = ResourceBundle.getBundle(SpeechWizardDialog.class.getName()); fiore@0: fiore@0: model = createModel(diagramToEdit); fiore@0: node = new Model.Node(); fiore@0: edge = new Model.Edge(); fiore@0: property = new Model.Property(); fiore@0: modifier = new Model.Modifier(); fiore@0: fiore@0: initWizardComponents(existingDiagrams,diagramToEdit); fiore@0: fiore@0: /* if the user is editing from an existing diagram, they have to choose a new name. They're switched * fiore@0: * directly to the diagram name panel so they have to enter a new name as they would otherwise * fiore@0: * not be allowed to proceed. */ fiore@0: if(diagramToEdit != null) fiore@0: dialog.getWizardComponents().setCurrentIndex(DIAGRAM_NAME); fiore@0: fiore@0: /* when the user clicks on the finish button they'll be prompted with a summary text area dialog * fiore@0: * describing what they have created so far and asking for a confirmation to proceed with the actual * fiore@0: * creation of the template. */ fiore@0: dialog.getWizardComponents().setFinishAction(new FinishAction(dialog.getWizardComponents()){ fiore@0: @Override fiore@0: public void performAction(){ fiore@0: String[] options = { fiore@0: resources.getString("dialog.summary.ok_button_label"), fiore@0: resources.getString("dialog.summary.cancel_button_label")}; fiore@0: int result = SpeechSummaryPane.showDialog( fiore@0: dialog, fiore@0: resources.getString("dialog.summary.title"), fiore@0: model.toString(), fiore@0: SpeechSummaryPane.OK_CANCEL_OPTION, fiore@0: options); fiore@0: fiore@0: if(result == SpeechSummaryPane.CANCEL){ // user wants to continue editing fiore@0: SoundFactory.getInstance().play(SoundEvent.CANCEL); fiore@0: return; fiore@0: } fiore@0: /* create the actual diagram (it will be return to the client class by execute()) */ fiore@0: diagram = createDiagram(model); fiore@0: dialog.dispose(); fiore@0: } fiore@0: }); fiore@0: } fiore@0: fiore@0: public Wizard(Frame frame, Collection existingDiagrams){ fiore@0: this(frame,existingDiagrams,null); fiore@0: } fiore@0: fiore@0: public Diagram execute(){ fiore@0: diagram = null; fiore@0: dialog.show(); fiore@0: if(diagram == null) fiore@0: SoundFactory.getInstance().play(SoundEvent.CANCEL); fiore@0: return diagram; fiore@0: } fiore@0: fiore@0: @SuppressWarnings("serial") fiore@0: private void initWizardComponents(Collection existingDiagrams, final Diagram diagramToEdit){ fiore@0: /* --- MAIN PANEL --- */ fiore@0: String[] choices = { fiore@0: resources.getString("panel.home.choice.diagram_name"), fiore@0: resources.getString("panel.home.choice.nodes"), fiore@0: resources.getString("panel.home.choice.edges"), fiore@0: }; fiore@0: int[] nexts = {DIAGRAM_NAME,NODES,EDGES}; fiore@0: fiore@0: /* panel for the selection of main tasks when creating the diagram: enter diagram name, create node and * fiore@0: * create edge. When a name is assigned an item is added to the selection which allows the user to finish * fiore@0: * the template creation, much as they would do by pressing the finish button. If the user edits an existing * fiore@0: * diagram they're prompted with a message to enter a new diagram name (as there cannot be two diagrams * fiore@0: * with the same name. When the user enters the name the message goes away */ fiore@0: add(HOME,new SelectWizardPanel( fiore@0: dialog.getWizardComponents(), fiore@0: resources.getString("panel.home.title.new"), fiore@0: Arrays.asList(choices), fiore@0: nexts, fiore@0: SpeechWizardPanel.DISABLE_SWITCH fiore@0: ){ fiore@0: @Override fiore@0: public void update(){ fiore@0: if(!model.diagramName.value.isEmpty()){ fiore@0: dialog.setFinishButtonEnabled(true); fiore@0: /* if the diagram has a name the template creation can finish. So add a selection item to the * fiore@0: * comboBox unless it's already there from a previous update (item count < 4 ) */ fiore@0: if(comboBox.getItemCount() < 4) fiore@0: ((DefaultComboBoxModel)comboBox.getModel()).addElement(resources.getString("panel.home.choice.finish")); fiore@0: } fiore@0: super.update(); fiore@0: } fiore@0: @Override fiore@0: public void next(){ fiore@0: if(comboBox.getSelectedIndex() == 3) fiore@0: dialog.getWizardComponents().getFinishButton().doClick(); fiore@0: else fiore@0: super.next(); fiore@0: } fiore@0: }); fiore@0: fiore@0: /* --- DIAGRAM NAME INPUT PANEL --- */ fiore@0: add(DIAGRAM_NAME, new TextWizardPanel( fiore@0: dialog.getWizardComponents(), fiore@0: resources.getString(diagramToEdit == null ? "panel.diagram_name.title" : "panel.diagram_name.title.editing_existing_diagram"), fiore@0: existingDiagrams, fiore@0: HOME, fiore@0: HOME, fiore@0: model.diagramName fiore@0: ){ fiore@0: @Override fiore@0: public void update(){ fiore@0: /* this is a little nasty trick to achieve the following: when the user creates a new diagram out of an already existing fiore@0: * one they're directly prompted with this panel. We want the name of the diagram to be there for awareness fiore@0: * but at the same time it must not be accepted by the program as it would otherwise clash with fiore@0: * with the starting diagam's. As the program accepts it when the text entered in the text field is equal to fiore@0: * model.diagramName.value (for when the user wants to re-edit the name of a diagram they're creating) we must fiore@0: * fill model.DiagramName.value with the name of the starting diagram to get it shown and spoken out but then fiore@0: * it's assigned the empty string not to let the user to go forward */ fiore@0: if(diagramToEdit != null && model.diagramName.value.isEmpty()){ fiore@0: model.diagramName.value = diagramToEdit.getName(); fiore@0: super.update(); fiore@0: model.diagramName.value = ""; fiore@0: }else{ fiore@0: super.update(); fiore@0: } fiore@0: } fiore@0: }); fiore@0: fiore@0: /* --- NODE ACTION SELECTION PANEL --- */ fiore@0: /* decide whether to add a new node or to edit/delete an existing node */ fiore@0: String[] nodeOptions = { fiore@0: resources.getString("panel.nodes.actions.add"), fiore@0: resources.getString("panel.nodes.actions.edit"), fiore@0: resources.getString("panel.nodes.actions.del"), fiore@0: resources.getString("panel.nodes.actions.finish")}; fiore@0: int[] nodeNexts = {NODE_TYPE,NODE_EDIT,NODE_DEL,HOME}; fiore@0: add(NODES, new ActionChooserPanel( fiore@0: dialog.getWizardComponents(), fiore@0: model.nodes.getNames(), fiore@0: resources.getString("panel.nodes.title"), fiore@0: nodeOptions, fiore@0: nodeNexts, fiore@0: HOME, fiore@0: node fiore@0: )); fiore@0: fiore@0: /* --- NODE TYPE NAME INPUT PANEL --- */ fiore@0: add(NODE_TYPE,new TextWizardPanel( fiore@0: dialog.getWizardComponents(), fiore@0: resources.getString("panel.node_name.title"), fiore@0: model.nodes.getNames(), fiore@0: NODE_SHAPE, fiore@0: NODES, fiore@0: node.type fiore@0: )); fiore@0: fiore@0: /* --- NODE TO DELETE SELECTION PANEL*/ fiore@0: add(NODE_DEL, new DeletePanel( fiore@0: dialog.getWizardComponents(), fiore@0: resources.getString("panel.node_del.title"), fiore@0: NODES, fiore@0: NODES, fiore@0: model.nodes)); fiore@0: fiore@0: /* -- NODE TO EDIT SELECTION PANEL */ fiore@0: add(NODE_EDIT, new EditPanel( fiore@0: dialog.getWizardComponents(), fiore@0: resources.getString("panel.node_edit.title"), fiore@0: NODE_TYPE, fiore@0: NODES, fiore@0: model.nodes, fiore@0: node fiore@0: )); fiore@0: fiore@0: ShapeType[] shapeTypes = ShapeType.values(); fiore@0: ArrayList shapeTypeNames = new ArrayList(shapeTypes.length); fiore@0: for(int i=0; i positionNames = new ArrayList(positions.length); fiore@0: for(int i=0; i max){ fiore@0: NarratorFactory.getInstance().speak(resources.getString("dialog.error.min_max")); fiore@0: }else{ fiore@0: super.next(); fiore@0: } fiore@0: } fiore@0: fiore@0: }); fiore@0: fiore@0: /* --- SELECT WHETHER THE EDGE MUST HAVE ARROW HEADS OR NOT --- */ fiore@0: String[] arrowHeadOptions = { fiore@0: resources.getString("panel.edge_yesno_arrow_head.actions.add"), fiore@0: resources.getString("panel.edge_yesno_arrow_head.actions.finish") fiore@0: }; fiore@0: int[] arrowHeadNexts = {EDGE_ARROW_HEAD,EDGES}; fiore@0: add(EDGE_YESNO_ARROW_HEAD,new SelectWizardPanel( fiore@0: dialog.getWizardComponents(), fiore@0: resources.getString("panel.edge_yesno_arrow_head.title"), fiore@0: Arrays.asList(arrowHeadOptions), fiore@0: arrowHeadNexts, fiore@0: EDGE_MAX_NODES fiore@0: ){ fiore@0: @Override fiore@0: public void next(){ fiore@0: if(comboBox.getSelectedIndex() == 1){ fiore@0: Model.Edge newEdge = new Model.Edge(); fiore@0: Model.copy(edge, newEdge); fiore@0: model.edges.put(newEdge.id,newEdge); fiore@0: } fiore@0: super.next(); fiore@0: } fiore@0: }); fiore@0: fiore@0: /* --- ARROW HEAD SELECTION PANEL --- */ fiore@0: add(EDGE_ARROW_HEAD, new ArrowHeadPanel()); fiore@0: fiore@0: add(LAST_PANEL, new DummyWizardPanel(dialog.getWizardComponents())); fiore@0: fiore@0: SpeechUtilities.changeTabListener((JComponent)dialog.getContentPane(), dialog); fiore@0: } fiore@0: fiore@0: private void add(int index, JWizardPanel panel){ fiore@0: dialog.getWizardComponents().addWizardPanel(index,panel); fiore@0: } fiore@0: fiore@0: private Diagram createDiagram(Model model){ fiore@0: /* create the node prototypes */ fiore@0: Node[] nodes = new Node[model.nodes.size()]; fiore@0: int i = 0; fiore@0: for(Model.Node n : model.nodes.values()){ fiore@0: nodes[i] = createDiagramNode(n); fiore@0: i++; fiore@0: } fiore@0: /* create the edge prototypes */ fiore@0: Edge[] edges = new Edge[model.edges.size()]; fiore@0: i = 0; fiore@0: for(Model.Edge e : model.edges.values()){ fiore@0: edges[i] = createDiagramEdge(e); fiore@0: i++; fiore@0: } fiore@0: return Diagram.newInstance(model.diagramName.value, nodes, edges, new SimpleShapePrototypePersistenceDelegate()); fiore@0: } fiore@0: fiore@0: private Node createDiagramNode(Model.Node n){ fiore@0: /* set up the properties */ fiore@0: LinkedHashMap> propertiesTypeDefinition = new LinkedHashMap>(); fiore@0: /* create the property type definition */ fiore@0: for(Model.Property modelProperty : n.properties.values()){ fiore@0: Set modifiersTypeDefinition = new LinkedHashSet(); fiore@0: for(Model.Modifier modifier : modelProperty.modifiers.values()) fiore@0: modifiersTypeDefinition.add(modifier.type.value); fiore@0: propertiesTypeDefinition.put(modelProperty.type.value, modifiersTypeDefinition); fiore@0: } fiore@0: NodeProperties properties = new NodeProperties(propertiesTypeDefinition); fiore@0: /* now that properties object is created attach the views on it */ fiore@0: for(Model.Property modelProperty : n.properties.values()){ fiore@0: PropertyView propertyView = new PropertyView( fiore@0: SimpleShapeNode.Position.valueOf(modelProperty.position.value), fiore@0: modelProperty.shape.value.isEmpty() ? fiore@0: /* doesn't really matter as position is inside and shape won't be taken into account */ fiore@0: SimpleShapeNode.ShapeType.Rectangle : fiore@0: SimpleShapeNode.ShapeType.valueOf(modelProperty.shape.value) fiore@0: ); fiore@0: properties.setView(modelProperty.type.value, propertyView); fiore@0: /* modifier view */ fiore@0: for(Model.Modifier modelModifier : modelProperty.modifiers.values()){ fiore@0: boolean bold = false; fiore@0: boolean italic = false; fiore@0: boolean underline = false; fiore@0: String prefix = ""; fiore@0: String suffix = ""; fiore@0: for(String value : modelModifier.format.values){ fiore@0: if(value.equals(resources.getString("modifier.format.bold"))){ fiore@0: bold = true; fiore@0: }else if(value.equals(resources.getString("modifier.format.underline"))){ fiore@0: underline = true; fiore@0: }else if(value.equals(resources.getString("modifier.format.italic"))){ fiore@0: italic = true; fiore@0: }else if(value.equals(resources.getString("modifier.format.prefix"))){ fiore@0: prefix = modelModifier.affix.values[PREFIX_INDEX]; fiore@0: }else if(value.equals(resources.getString("modifier.format.suffix"))){ fiore@0: suffix = modelModifier.affix.values[SUFFIX_INDEX]; fiore@0: } fiore@0: } fiore@0: ModifierView modifierView = new ModifierView(underline,bold,italic,prefix,suffix); fiore@0: properties.getModifiers(modelProperty.type.value).setView(modelModifier.type.value, modifierView); fiore@0: } fiore@0: } fiore@0: return SimpleShapeNode.getInstance( fiore@0: SimpleShapeNode.ShapeType.valueOf(n.shape.value), fiore@0: n.type.value, fiore@0: properties); fiore@0: } fiore@0: fiore@0: private Edge createDiagramEdge(Model.Edge e){ fiore@0: /* create the arrow head array out of the string stored in the model */ fiore@0: ArrowHead[] arrowHeads = new ArrowHead[e.arrowHeads.values.length]; fiore@0: for(int i=0; i elementNames, String title, String[] options, int[] nexts, int previous, Model.Element temporaryElement){ fiore@0: super(wizardComponents,title,OWN_SWITCH, previous); fiore@0: this.options = options; fiore@0: comboBoxModel = new DefaultComboBoxModel(); fiore@0: comboBoxModel.addElement(options[0]); fiore@0: comboBoxModel.addElement(options[3]); fiore@0: comboBox = new LoopComboBox(comboBoxModel); fiore@0: comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); fiore@0: layoutComponents(comboBox); fiore@0: this.elementNames = elementNames; fiore@0: this.temporaryElement = temporaryElement; fiore@0: this.nexts = nexts; fiore@0: } fiore@0: fiore@0: @Override fiore@0: public void update(){ fiore@0: if(elementNames.isEmpty() && comboBoxModel.getSize() == 4){ fiore@0: comboBoxModel.removeElement(options[1]); fiore@0: comboBoxModel.removeElement(options[2]); fiore@0: }else if(!elementNames.isEmpty() && comboBoxModel.getSize() == 2){ fiore@0: comboBoxModel.insertElementAt(options[1],1); fiore@0: comboBoxModel.insertElementAt(options[2],2); fiore@0: } fiore@0: super.update(); fiore@0: } fiore@0: fiore@0: @Override fiore@0: public void next(){ fiore@0: /* if the selection was add element, then we clear the temporary holder */ fiore@0: if(comboBox.getSelectedIndex() == 0) fiore@0: temporaryElement.clear(); fiore@0: /* jump to the selected next step, works both when it's only add/finish and when it's add/delete/edit/finish */ fiore@0: for(int i=0; i elementNames; fiore@0: DefaultComboBoxModel comboBoxModel; fiore@0: String[] options; fiore@0: Model.Element temporaryElement; fiore@0: int[] nexts; fiore@0: } fiore@0: fiore@0: @SuppressWarnings("serial") fiore@0: private static class DeletePanel extends SpeechWizardPanel { fiore@0: DeletePanel(JWizardComponents wizardComponents,String title, int next, int previous, ModelMap elements){ fiore@0: super(wizardComponents,title,next, previous); fiore@0: this.elements = elements; fiore@0: comboBox = new LoopComboBox(); fiore@0: comboBox.addItemListener(SpeechUtilities.getSpeechComboBoxItemListener()); fiore@0: layoutComponents(comboBox); fiore@0: } fiore@0: fiore@0: @Override fiore@0: public void update(){ fiore@0: String[] options = new String[elements.values().size()]; fiore@0: options = elements.getNames().toArray(options); fiore@0: comboBox.setModel(new DefaultComboBoxModel(options)); fiore@0: super.update(); fiore@0: } fiore@0: fiore@0: /** fiore@0: * the default behaviour is to delete the selected element fiore@0: */ fiore@0: @Override fiore@0: public void next(){ fiore@0: Model.Element elementToDelete = null; fiore@0: for(Model.Element element : elements.values()){ fiore@0: if(element.type.value.equals(comboBox.getSelectedItem())){ fiore@0: elementToDelete = element; fiore@0: break; fiore@0: } fiore@0: } fiore@0: Object o = elements.remove(elementToDelete.id); fiore@0: assert(o != null); fiore@0: super.next(); fiore@0: } fiore@0: fiore@0: JComboBox comboBox; fiore@0: ModelMap elements; fiore@0: } fiore@0: fiore@0: @SuppressWarnings("serial") fiore@0: private static class EditPanel extends DeletePanel { fiore@0: EditPanel(JWizardComponents wizardComponents, fiore@0: String title, fiore@0: int next, fiore@0: int previous, fiore@0: ModelMap elements, fiore@0: Model.Element temporaryHolder){ fiore@0: super(wizardComponents, title,next, previous,elements); fiore@0: this.temporaryHolder = temporaryHolder; fiore@0: this.next = next; fiore@0: } fiore@0: fiore@0: @Override fiore@0: public void next(){ fiore@0: Model.Element selected = null; fiore@0: for(Model.Element e : elements.values()){ fiore@0: if(e.type.value.equals(comboBox.getSelectedItem())){ fiore@0: selected = e; fiore@0: break; fiore@0: } fiore@0: } fiore@0: fiore@0: Model.copy(selected, temporaryHolder); fiore@0: switchPanel(next); fiore@0: } fiore@0: fiore@0: int next; fiore@0: Model.Element temporaryHolder; fiore@0: } fiore@0: fiore@0: @SuppressWarnings("serial") fiore@0: private class FormatWizardPanel extends SpeechWizardPanel{ fiore@0: FormatWizardPanel(){ fiore@0: super(dialog.getWizardComponents(),resources.getString("panel.modifier_format.title"),MODIFIERS,MODIFIER_TYPE); fiore@0: String values[] = { fiore@0: resources.getString("modifier.format.bold"), fiore@0: resources.getString("modifier.format.underline"), fiore@0: resources.getString("modifier.format.italic"), fiore@0: resources.getString("modifier.format.prefix"), fiore@0: resources.getString("modifier.format.suffix"), fiore@0: }; fiore@0: fiore@0: checkBoxes = new JCheckBox[values.length]; fiore@0: checkBoxPanel = new JPanel(new GridLayout(0, 1)); fiore@0: for(int i=0; i