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