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 extends Model.Element> 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 extends Model.Element> 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 extends Model.Element> 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