/* * JFLAP - Formal Languages and Automata Package * * * Susan H. Rodger * Computer Science Department * Duke University * August 27, 2009 * Copyright (c) 2002-2009 * All rights reserved. * JFLAP is open source software. Please see the LICENSE for terms. * */ package gui.editor; import gui.environment.AutomatonEnvironment; import gui.environment.Environment; import gui.environment.EnvironmentFrame; import gui.environment.tag.CriticalTag; import gui.viewer.AutomatonDrawer; import gui.viewer.AutomatonPane; import gui.viewer.CurvedArrow; import java.awt.Color; import java.awt.Component; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.geom.QuadCurve2D; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Iterator; import javax.swing.Icon; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.KeyStroke; import javax.swing.SingleSelectionModel; import automata.Automaton; import automata.Note; import automata.State; import automata.StateRenamer; import automata.Transition; import automata.fsa.FSALabelHandler; import automata.fsa.FSATransition; import automata.graph.AutomatonGraph; import automata.graph.LayoutAlgorithm; import automata.graph.layout.GEMLayoutAlgorithm; import automata.turing.TMTransition; import automata.turing.TMState; import automata.turing.TuringMachine; import debug.EDebug; /** * The arrow tool is used mostly for editing existing objects. * * @author Thomas Finley, Henry Qin */ public class ArrowTool extends Tool { /** * Instantiates a new arrow tool. * * @param view * the view where the automaton is drawn * @param drawer * the object that draws the automaton * @param creator * the transition creator used for editing transitions */ public ArrowTool(AutomatonPane view, AutomatonDrawer drawer, TransitionCreator creator) { super(view, drawer); this.creator = creator; } /** * Instantiates a new arrow tool. * * @param view * the view where the automaton is drawn * @param drawer * the object that draws the automaton */ public ArrowTool(AutomatonPane view, AutomatonDrawer drawer) { super(view, drawer); this.creator = TransitionCreator.creatorForAutomaton(getAutomaton(), getView()); } /** * Gets the tool tip for this tool. * * @return the tool tip for this tool */ public String getToolTip() { return "Attribute Editor"; } /** * Returns the tool icon. * * @return the arrow tool icon */ protected Icon getIcon() { java.net.URL url = getClass().getResource("/ICON/arrow.gif"); return new javax.swing.ImageIcon(url); } /** * On a mouse click, if this is a double click over a transition edit the * transition. If this was a single click, then we select the transition. * * @param event * the mouse event */ public void mouseClicked(MouseEvent event) { if (event.getClickCount() == 1){ Transition trans = getDrawer().transitionAtPoint(event.getPoint()); if (trans != null){ if (trans.isSelected){ trans.isSelected = false; selectedTransition = null; } else{ if (selectedTransition != null) selectedTransition.isSelected = false; trans.isSelected = true; selectedTransition = trans; } return; } } Transition trans = getDrawer().transitionAtPoint(event.getPoint()); if (trans == null){ Rectangle bounds; bounds = new Rectangle(0, 0, -1, -1); getView().getDrawer().getAutomaton().selectStatesWithinBounds(bounds); getView().repaint(); return; } EDebug.print("Beginning to Edit with creator "+ creator.getClass()); creator.editTransition(trans, event.getPoint()); } /** * Possibly show a popup menu. * * @param event * the mouse event */ protected void showPopup(MouseEvent event) { // Should we show a popup menu? if (event.isPopupTrigger()) { Point p = getView().transformFromAutomatonToView(event.getPoint()); if (lastClickedState != null && shouldShowStatePopup()) { stateMenu.show(lastClickedState, getView(), p); } else { emptyMenu.show(getView(), p); } } lastClickedState = null; lastClickedTransition = null; } /** * On a mouse press, allows the state to be dragged about unless this is a * popup trigger. */ public void mousePressed(MouseEvent event) { if (getDrawer().getAutomaton().getEnvironmentFrame() !=null) ((AutomatonEnvironment)getDrawer().getAutomaton().getEnvironmentFrame().getEnvironment()).saveStatus(); else EDebug.print("I cannot preserve what you ask"); initialPointClick.setLocation(event.getPoint()); lastClickedState = getDrawer().stateAtPoint(event.getPoint()); if (lastClickedState == null) lastClickedTransition = getDrawer().transitionAtPoint( event.getPoint()); // Should we show a popup menu? if (event.isPopupTrigger()) showPopup(event); if (lastClickedState != null) { initialPointState.setLocation(lastClickedState.getPoint()); if(!lastClickedState.isSelected()){ Rectangle bounds = new Rectangle(0, 0, -1, -1); getView().getDrawer().getAutomaton().selectStatesWithinBounds(bounds); getView().getDrawer().setSelectionBounds(bounds); lastClickedState.setSelect(true); } getView().repaint(); } else if (lastClickedTransition != null) { initialPointClick.setLocation(event.getPoint()); } else { ArrayList notes = getDrawer().getAutomaton().getNotes(); for(int k = 0; k < notes.size(); k++){ ((Note)notes.get(k)).setEditable(false); ((Note)notes.get(k)).setEnabled(false); ((Note)notes.get(k)).setCaretColor(new Color(255, 255, 150)); } Rectangle bounds = new Rectangle(0, 0, -1, -1); getView().getDrawer().getAutomaton().selectStatesWithinBounds(bounds); getView().getDrawer().setSelectionBounds(bounds); } //reset the selectedTransition after an Undo has happened. Transition[] trans = getAutomaton().getTransitions(); for (int i = 0; i < trans.length; i++) if (trans[i].isSelected){ selectedTransition = trans[i]; return; } selectedTransition = null; } /** * Returns if the state popup menu should be shown whenever applicable. * * @return true if the state menu should be popped up, false * if it should not be... returns true by default */ protected boolean shouldShowStatePopup() { return true; } /** * On a mouse drag, possibly move a state if the first press was on a state. */ public void mouseDragged(MouseEvent event) { if (lastClickedState != null) { if (event.isPopupTrigger()) return; Point p = event.getPoint(); State[] states = getView().getDrawer().getAutomaton().getStates(); for(int k = 0; k < states.length; k++){ State curState = states[k]; if(curState.isSelected()){ int x = curState.getPoint().x + p.x - initialPointClick.x; int y = curState.getPoint().y + p.y - initialPointClick.y; curState.getPoint().setLocation(x, y); curState.setPoint(curState.getPoint()); } } initialPointClick = p; getView().repaint(); } else if (lastClickedTransition != null) { if (event.isPopupTrigger()) return; Point p = event.getPoint(); int x = p.x - initialPointClick.x; int y = p.y - initialPointClick.y; State f = lastClickedTransition.getFromState(), t = lastClickedTransition .getToState(); if (f==t){ //uncomment this code for Transitions movement /* double circlex = (p.x-f.getPoint().x); double circley = (p.y-f.getPoint().y); double angle = Math.atan2(circley, circlex); Point from = getView().getDrawer().pointOnState(f, angle+Math.PI*.166); Point to = getView().getDrawer().pointOnState(f, angle-Math.PI*.166); Transition[] trans = getAutomaton().getTransitionsFromStateToState(f, t); for (int n = 0; n < trans.length; n++) { CurvedArrow arrow = (CurvedArrow) getView().getDrawer().transitionToArrowMap.get(trans[n]); // arrow.setStart(from); // arrow.setEnd(to); getView().getDrawer().selfTransitionMap.put(trans[n], angle); getView().getDrawer().arrowToTransitionMap.put(arrow, trans[n]); getView().getDrawer().transitionToArrowMap.put(trans[n], arrow); } */ } if (f != t) { //f.getPoint().translate(x, y); //f.setPoint(f.getPoint()); // Don't want self loops moving twice the speed... //t.getPoint().translate(x, y); //t.setPoint(t.getPoint()); double circlex = (p.x-f.getPoint().x); double circley = (p.y-f.getPoint().y); //double angle = Math.atan2(circley, circlex); //Point from = getView().getDrawer().pointOnState(f, angle+Math.PI*.166); //Point to = getView().getDrawer().pointOnState(f, angle-Math.PI*.166); Transition[] trans = getAutomaton().getTransitionsFromStateToState(f, t); for (int n = 0; n < trans.length; n++) { CurvedArrow arrow = (CurvedArrow) getView().getDrawer().transitionToArrowMap.get(trans[n]); //uncomment this code for Transitions movement /* float centerx = (t.getPoint().x+f.getPoint().x)/2; float centery = (t.getPoint().y+f.getPoint().y)/2; float pvecx = p.x-centerx; float pvecy = p.y-centery; float svecx = t.getPoint().x-centerx; float svecy = t.getPoint().y-centery; float dprod = pvecx*svecx+pvecy*svecy; dprod = dprod/(float) Math.abs((Math.sqrt(pvecx*pvecx+pvecy*pvecy)))/(float) Math.abs((Math.sqrt(svecx*svecx+svecy*svecy))); float theta = (float) Math.acos(dprod); float curv = (float) (Math.sqrt(pvecx*pvecx+pvecy*pvecy)*Math.sin(theta))/10; */ //float curv = (float) Math.sqrt((p.x-centerx)*(p.x-centerx)+(p.y-centery)*(p.y-centery))/10; //Float curv = (float) -(p.y-(f.getPoint().y+t.getPoint().y)/2)/10; //if (curv>=0){ //uncomment this code for Transitions /* arrow.setCurvy(curv+n); getView().getDrawer().curveTransitionMap.put(trans[n], curv); */ //}else{ // arrow.setCurvy(-(curv-n)); // getView().getDrawer().curveTransitionMap.put(trans[n], -curv); //} //QuadCurve2D curve = arrow.getCurve(); //curve.setCurve(curve.getX1(), curve.getY1(), p.x, p.y, curve.getX2(), curve.getY2()); getView().getDrawer().arrowToTransitionMap.put(arrow, trans[n]); getView().getDrawer().transitionToArrowMap.put(trans[n], arrow); } } initialPointClick.setLocation(p); getView().repaint(); //EDebug.print(getView().getDrawer().selfTransitionMap); } else{ Rectangle bounds; int nowX = event.getPoint().x; int nowY = event.getPoint().y; int leftX = initialPointClick.x; int topY = initialPointClick.y; if(nowX < initialPointClick.x) leftX = nowX; if(nowY < initialPointClick.y) topY = nowY; bounds = new Rectangle(leftX, topY, Math.abs(nowX-initialPointClick.x), Math.abs(nowY-initialPointClick.y)); if (!transitionInFlux){ getView().getDrawer().getAutomaton().selectStatesWithinBounds(bounds); getView().getDrawer().setSelectionBounds(bounds); } getView().repaint(); } //Deal with transition dragging here if (selectedTransition != null){ //simply set ...but we need to get the initial point to be clever CurvedArrow ca = (CurvedArrow)getView().getDrawer().transitionToArrowMap.get(selectedTransition); Point myClickP = event.getPoint(); Point2D control = ca.getCurve().getCtrlPt(); if (transitionInFlux || Math.sqrt((control.getX() - myClickP.x)*(control.getX() - myClickP.x) + (control.getY() - myClickP.y)*(control.getY() - myClickP.y)) < 15){ selectedTransition.setControl(myClickP); // System.out.println("Move it damn it"); ca.refreshCurve(); transitionInFlux = true; return; } } } private boolean transitionInFlux = false; /** * On a mouse release, sets the tool to the "virgin" state. */ public void mouseReleased(MouseEvent event) { transitionInFlux = false; if (event.isPopupTrigger()) showPopup(event); State[] states = getView().getDrawer().getAutomaton().getStates(); int count = 0; for(int k = 0; k < states.length; k++){ if(states[k].isSelected()){ count++; } } Rectangle bounds = getView().getDrawer().getSelectionBounds(); if(count == 1 && bounds.isEmpty() && lastClickedState!=null) lastClickedState.setSelect(false); bounds = new Rectangle(0, 0, -1, -1); getView().getDrawer().setSelectionBounds(bounds); lastClickedState = null; lastClickedTransition = null; getView().repaint(); } /** * Returns the key stroke that will activate this tool. * * @return the key stroke that will activate this tool */ public KeyStroke getKey() { return KeyStroke.getKeyStroke('a'); } /** * Returns true if only changing the final stateness of a state should be * allowed in the state menu. */ public boolean shouldAllowOnlyFinalStateChange() { return false; } /** * The contextual menu class for editing states. */ /* * I changed this from private class to protected class so I can * remove the "Final State" option from Moore and Mealy machines. */ protected class StateMenu extends JPopupMenu implements ActionListener { public StateMenu() { makeFinal = new JCheckBoxMenuItem("Final"); makeFinal.addActionListener(this); this.add(makeFinal); makeInitial = new JCheckBoxMenuItem("Initial"); changeLabel = new JMenuItem("Change Label"); deleteLabel = new JMenuItem("Clear Label"); deleteAllLabels = new JMenuItem("Clear All Labels"); editBlock = new JMenuItem("Edit Block"); copyBlock = new JMenuItem("Duplicate Block"); replaceSymbol = new JMenuItem("Replace Symbol"); setName = new JMenuItem("Set Name"); if (shouldAllowOnlyFinalStateChange()) return; makeInitial.addActionListener(this); changeLabel.addActionListener(this); deleteLabel.addActionListener(this); deleteAllLabels.addActionListener(this); editBlock.addActionListener(this); setName.addActionListener(this); copyBlock.addActionListener(this); replaceSymbol.addActionListener(this); this.add(makeInitial); this.add(changeLabel); this.add(deleteLabel); this.add(deleteAllLabels); this.add(setName); } public void show(State state, Component comp, Point at) { this.remove(editBlock); this.state = state; // if (state.getInternalName() != null) { if (state instanceof TMState) { this.add(editBlock); this.add(copyBlock); editBlock.setEnabled(true); copyBlock.setEnabled(true); this.add(replaceSymbol); replaceSymbol.setEnabled(true); } makeFinal.setSelected(getAutomaton().isFinalState(state)); makeInitial.setSelected(getAutomaton().getInitialState() == state); deleteLabel.setEnabled(state.getLabel() != null); show(comp, at.x, at.y); } public void actionPerformed(ActionEvent e) { JMenuItem item = (JMenuItem) e.getSource(); if (getDrawer().getAutomaton().getEnvironmentFrame() !=null) ((AutomatonEnvironment)getDrawer().getAutomaton().getEnvironmentFrame().getEnvironment()).saveStatus(); if (item == makeFinal) { if (item.isSelected()) getAutomaton().addFinalState(state); else getAutomaton().removeFinalState(state); } else if (item == makeInitial) { if (!item.isSelected()) state = null; getAutomaton().setInitialState(state); } else if (item == changeLabel) { String oldlabel = state.getLabel(); oldlabel = oldlabel == null ? "" : oldlabel; String label = (String) JOptionPane.showInputDialog(this, "Input a new label, or \n" + "set blank to remove the label", "New Label", JOptionPane.QUESTION_MESSAGE, null, null, oldlabel); if (label == null) return; if (label.equals("")) label = null; state.setLabel(label); } else if (item == deleteLabel) { state.setLabel(null); } else if (item == deleteAllLabels) { State[] states = getAutomaton().getStates(); for (int i = 0; i < states.length; i++) states[i].setLabel(null); } else if (item == editBlock) { //this implies that this was a TMState to begin with, because only TM states would have this menu option //not sure why need highest level automaton, but okay TMState parent = (TMState) state; while (((TuringMachine)parent.getAutomaton()).getParent() != null) { parent = ((TuringMachine)parent.getAutomaton()).getParent(); } EditBlockPane editor = new EditBlockPane(((TMState)state).getInnerTM()); //give it a Turing Machine //just edit the Automaton directly; there is no need for a repaint either, because the other guy does not paint it EnvironmentFrame rootFrame = parent.getAutomaton().getEnvironmentFrame(); editor.setBlock(state); Environment envir = rootFrame.getEnvironment(); envir.add(editor, "Edit Block", new CriticalTag() { }); envir.setActive(editor); } else if (item == setName) { String oldName = state.getName(); oldName = oldName == null ? "" : oldName; String name = (String) JOptionPane.showInputDialog(this, "Input a new name, or \n" + "set blank to remove the name", "New Name", JOptionPane.QUESTION_MESSAGE, null, null, oldName); if (name == null) return; if (name.equals("")) name = null; state.setName(name); }else if (item == copyBlock) { //MERLIN MERLIN MERLIN MERLIN MERLIN// // TMState buffer = ((TuringMachine) getAutomaton()).createTMState((Point)state.getPoint()); //again, we assume that the cast will work, since copyBlock hould never be there except with Turing. TMState buffer = ((TuringMachine) getAutomaton()).createTMState(new Point(state.getPoint().x+4, state.getPoint().y)); //again, we assume that the cast will work, since copyBlock hould never be there except with Turing. buffer.setInnerTM((TuringMachine)((TMState) state).getInnerTM().clone()); //all states have an inner TM, although this inner TM might have zero states within it, in which case it acts as a simple state. } else if (item == replaceSymbol) { assert state instanceof TMState; String replaceWith = null; String toReplace = null; Object old = JOptionPane.showInputDialog(null, "Find"); if (old == null) return; if(old instanceof String){ toReplace = (String)old; } Object newString = JOptionPane.showInputDialog(null, "Replace With"); if (newString == null) return; if(newString instanceof String){ replaceWith = (String)newString; } replaceCharactersInBlock((TMState) state, toReplace, replaceWith); } getView().repaint(); } private void replaceCharactersInBlock(TMState start, String toReplace, String replaceWith){ //this shall be a recursive method, replacing the inside and then the out TuringMachine tm = start.getInnerTM(); for (int i = 0; i < tm.getStates().length; i++) replaceCharactersInBlock((TMState)tm.getStates()[i], toReplace, replaceWith); Transition[] trans = tm.getTransitions(); for (int i = 0; i < trans.length; ++i){ TMTransition tmTrans = (TMTransition)trans[i]; for(int k = 0; k < tmTrans.tapes(); k++){ String read = tmTrans.getRead(k); tmTrans.setRead(k, read.replaceAll(toReplace, replaceWith)); String write = tmTrans.getWrite(k); tmTrans.setWrite (k,write.replaceAll(toReplace, replaceWith)); } } } private State state; /* * Changed this from private to protected so I can remove * "Final State" option from Moore and Mealy machines. */ protected JCheckBoxMenuItem makeFinal, makeInitial; private JMenuItem changeLabel, deleteLabel, deleteAllLabels, editBlock, copyBlock, replaceSymbol, setName; } /** * The contextual menu class for editing transitions. */ private class TransitionMenu extends JPopupMenu { } /** * The contextual menu class for context clicks in blank space. */ private class EmptyMenu extends JPopupMenu implements ActionListener { public EmptyMenu() { stateLabels = new JCheckBoxMenuItem("Display State Labels"); stateLabels.addActionListener(this); this.add(stateLabels); layoutGraph = new JMenuItem("Layout Graph"); if (!(ArrowTool.this instanceof ArrowDisplayOnlyTool)) { layoutGraph.addActionListener(this); this.add(layoutGraph); } renameStates = new JMenuItem("Rename States"); if (!(ArrowTool.this instanceof ArrowDisplayOnlyTool)) { renameStates.addActionListener(this); this.add(renameStates); } addNote = new JMenuItem("Add Note"); if (!(ArrowTool.this instanceof ArrowDisplayOnlyTool)) { addNote.addActionListener(this); this.add(addNote); } // BEGIN SJK add adaptView = new JCheckBoxMenuItem("Auto-Zoom"); if (!(ArrowTool.this instanceof ArrowDisplayOnlyTool)) { adaptView.addActionListener(this); this.add(adaptView); } // END SJK add } public void show(Component comp, Point at) { stateLabels.setSelected(getDrawer().doesDrawStateLabels()); adaptView.setSelected(getView().getAdapt()); myPoint = at; show(comp, at.x, at.y); } public void actionPerformed(ActionEvent e) { JMenuItem item = (JMenuItem) e.getSource(); if (item == stateLabels) { getView().getDrawer().shouldDrawStateLabels(item.isSelected()); } else if (item == layoutGraph) { AutomatonGraph g = new AutomatonGraph(getAutomaton()); LayoutAlgorithm alg = new GEMLayoutAlgorithm(); alg.layout(g, null); g.moveAutomatonStates(); getView().fitToBounds(30); } else if (item == renameStates) { ((AutomatonEnvironment)getDrawer().getAutomaton().getEnvironmentFrame().getEnvironment()).saveStatus(); StateRenamer.rename(getAutomaton()); } else if (item == adaptView) { getView().setAdapt(item.isSelected()); } else if (item == addNote) { ((AutomatonEnvironment)getDrawer().getAutomaton().getEnvironmentFrame().getEnvironment()).saveStatus(); Note newNote = new Note(myPoint, "insert_text"); newNote.initializeForView(getView()); getView().getDrawer().getAutomaton().addNote(newNote); } getView().repaint(); //boolean selected = adaptView.isSelected(); emptyMenu = new EmptyMenu(); //adaptView.setSelected(selected); } private Point myPoint; private JCheckBoxMenuItem stateLabels; private Note curNote; private JMenuItem layoutGraph; private JMenuItem addNote; private JMenuItem renameStates, adaptView; } /** The transition creator for editing transitions. */ private TransitionCreator creator; /** The state that was last clicked. */ private State lastClickedState = null; /** The transition that was last clicked. */ private Transition lastClickedTransition = null; /** The note that was last clicked. */ private Note lastClickedNote = null; /** The initial point of the state. */ private Point initialPointState = new Point(); /** The initial point of the click. */ private Point initialPointClick = new Point(); /** The state menu. */ /* * I changed it to protected because I needed to mess with * it in a subclass. This is to remove the "Final State" * option in Moore and Mealy machines. */ protected StateMenu stateMenu = new StateMenu(); /** The transition menu. */ private TransitionMenu transitionMenu = new TransitionMenu(); /** The empty menu. */ private EmptyMenu emptyMenu = new EmptyMenu(); private Transition selectedTransition = null; }