added svn:ignores
[debian/openrocket] / src / net / sf / openrocket / gui / scalefigure / RocketPanel.java
1 package net.sf.openrocket.gui.scalefigure;
2
3
4 import net.miginfocom.swing.MigLayout;
5 import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
6 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
7 import net.sf.openrocket.aerodynamics.FlightConditions;
8 import net.sf.openrocket.aerodynamics.WarningSet;
9 import net.sf.openrocket.document.OpenRocketDocument;
10 import net.sf.openrocket.document.Simulation;
11 import net.sf.openrocket.gui.adaptors.DoubleModel;
12 import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
13 import net.sf.openrocket.gui.components.BasicSlider;
14 import net.sf.openrocket.gui.components.StageSelector;
15 import net.sf.openrocket.gui.components.UnitSelector;
16 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
17 import net.sf.openrocket.gui.figureelements.CGCaret;
18 import net.sf.openrocket.gui.figureelements.CPCaret;
19 import net.sf.openrocket.gui.figureelements.Caret;
20 import net.sf.openrocket.gui.figureelements.RocketInfo;
21 import net.sf.openrocket.gui.main.SimulationWorker;
22 import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
23 import net.sf.openrocket.masscalc.BasicMassCalculator;
24 import net.sf.openrocket.masscalc.MassCalculator;
25 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
26 import net.sf.openrocket.rocketcomponent.Configuration;
27 import net.sf.openrocket.rocketcomponent.Rocket;
28 import net.sf.openrocket.rocketcomponent.RocketComponent;
29 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
30 import net.sf.openrocket.simulation.FlightData;
31 import net.sf.openrocket.simulation.listeners.SimulationListener;
32 import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
33 import net.sf.openrocket.simulation.listeners.system.InterruptListener;
34 import net.sf.openrocket.unit.UnitGroup;
35 import net.sf.openrocket.util.ChangeSource;
36 import net.sf.openrocket.util.Chars;
37 import net.sf.openrocket.util.Coordinate;
38 import net.sf.openrocket.util.MathUtil;
39 import net.sf.openrocket.util.Prefs;
40
41 import javax.swing.AbstractAction;
42 import javax.swing.Action;
43 import javax.swing.JComboBox;
44 import javax.swing.JLabel;
45 import javax.swing.JPanel;
46 import javax.swing.JSlider;
47 import javax.swing.JToggleButton;
48 import javax.swing.JViewport;
49 import javax.swing.SwingUtilities;
50 import javax.swing.event.ChangeEvent;
51 import javax.swing.event.ChangeListener;
52 import javax.swing.event.TreeSelectionEvent;
53 import javax.swing.event.TreeSelectionListener;
54 import javax.swing.tree.TreePath;
55 import javax.swing.tree.TreeSelectionModel;
56 import java.awt.Dimension;
57 import java.awt.Font;
58 import java.awt.Point;
59 import java.awt.event.ActionEvent;
60 import java.awt.event.InputEvent;
61 import java.awt.event.MouseEvent;
62 import java.util.ArrayList;
63 import java.util.Collection;
64 import java.util.List;
65 import java.util.concurrent.Executor;
66 import java.util.concurrent.Executors;
67 import java.util.concurrent.ThreadFactory;
68
69 /**
70  * A JPanel that contains a RocketFigure and buttons to manipulate the figure. 
71  * 
72  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
73  */
74 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
75         
76         private final RocketFigure figure;
77         private final ScaleScrollPane scrollPane;
78         
79         private JLabel infoMessage;
80         
81         private TreeSelectionModel selectionModel = null;
82         
83
84         /* Calculation of CP and CG */
85         private AerodynamicCalculator aerodynamicCalculator;
86         private MassCalculator massCalculator;
87         
88
89         private final OpenRocketDocument document;
90         private final Configuration configuration;
91         
92         private Caret extraCP = null;
93         private Caret extraCG = null;
94         private RocketInfo extraText = null;
95         
96
97         private double cpAOA = Double.NaN;
98         private double cpTheta = Double.NaN;
99         private double cpMach = Double.NaN;
100         private double cpRoll = Double.NaN;
101         
102         // The functional ID of the rocket that was simulated
103         private int flightDataFunctionalID = -1;
104         private String flightDataMotorID = null;
105         
106
107         private SimulationWorker backgroundSimulationWorker = null;
108         private boolean dirty = false;
109         
110         private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
111         
112
113         /**
114          * The executor service used for running the background simulations.
115          * This uses a fixed-sized thread pool for all background simulations
116          * with all threads in daemon mode and with minimum priority.
117          */
118         private static final Executor backgroundSimulationExecutor;
119         static {
120                 backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
121                                 new ThreadFactory() {
122                                         private ThreadFactory factory = Executors.defaultThreadFactory();
123                                         
124                                         @Override
125                                         public Thread newThread(Runnable r) {
126                                                 Thread t = factory.newThread(r);
127                                                 t.setDaemon(true);
128                                                 t.setPriority(Thread.MIN_PRIORITY);
129                                                 return t;
130                                         }
131                                 });
132         }
133         
134         
135         public RocketPanel(OpenRocketDocument document) {
136                 
137                 this.document = document;
138                 configuration = document.getDefaultConfiguration();
139                 
140                 // TODO: FUTURE: calculator selection
141                 aerodynamicCalculator = new BarrowmanCalculator();
142                 massCalculator = new BasicMassCalculator();
143                 
144                 // Create figure and custom scroll pane
145                 figure = new RocketFigure(configuration);
146                 
147                 scrollPane = new ScaleScrollPane(figure) {
148                         @Override
149                         public void mouseClicked(MouseEvent event) {
150                                 handleMouseClick(event);
151                         }
152                 };
153                 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
154                 scrollPane.setFitting(true);
155                 
156                 createPanel();
157                 
158                 configuration.addChangeListener(new ChangeListener() {
159                         @Override
160                         public void stateChanged(ChangeEvent e) {
161                                 System.out.println("Configuration changed, calling updateFigure");
162                                 updateExtras();
163                                 figure.updateFigure();
164                         }
165                 });
166         }
167         
168         
169         /**
170          * Creates the layout and components of the panel.
171          */
172         private void createPanel() {
173                 setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
174                 
175                 setPreferredSize(new Dimension(800, 300));
176                 
177
178                 //// Create toolbar
179                 
180                 // Side/back buttons
181                 FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
182                 action.putValue(Action.NAME, "Side view");
183                 action.putValue(Action.SHORT_DESCRIPTION, "Side view");
184                 JToggleButton toggle = new JToggleButton(action);
185                 add(toggle, "spanx, split");
186                 
187                 action = new FigureTypeAction(RocketFigure.TYPE_BACK);
188                 action.putValue(Action.NAME, "Back view");
189                 action.putValue(Action.SHORT_DESCRIPTION, "Rear view");
190                 toggle = new JToggleButton(action);
191                 add(toggle, "gap rel");
192                 
193
194                 // Zoom level selector
195                 ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
196                 add(scaleSelector);
197                 
198
199
200                 // Stage selector
201                 StageSelector stageSelector = new StageSelector(configuration);
202                 add(stageSelector, "");
203                 
204
205
206                 // Motor configuration selector
207                 
208                 JLabel label = new JLabel("Motor configuration:");
209                 label.setHorizontalAlignment(JLabel.RIGHT);
210                 add(label, "growx, right");
211                 add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap");
212                 
213
214
215
216
217                 // Create slider and scroll pane
218                 
219                 DoubleModel theta = new DoubleModel(figure, "Rotation",
220                                 UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
221                 UnitSelector us = new UnitSelector(theta, true);
222                 us.setHorizontalAlignment(JLabel.CENTER);
223                 add(us, "alignx 50%, growx");
224                 
225                 // Add the rocket figure
226                 add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap");
227                 
228
229                 // Add rotation slider
230                 // Minimum size to fit "360deg"
231                 JLabel l = new JLabel("360" + Chars.DEGREE);
232                 Dimension d = l.getPreferredSize();
233                 
234                 add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true),
235                                 "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy");
236                 
237
238                 infoMessage = new JLabel("<html>" +
239                                 "Click to select &nbsp;&nbsp; " +
240                                 "Shift+click to select other &nbsp;&nbsp; " +
241                                 "Double-click to edit &nbsp;&nbsp; " +
242                                 "Click+drag to move");
243                 infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
244                 add(infoMessage, "skip, span, gapleft 25, wrap");
245                 
246                 addExtras();
247         }
248         
249         
250
251         public RocketFigure getFigure() {
252                 return figure;
253         }
254         
255         public AerodynamicCalculator getAerodynamicCalculator() {
256                 return aerodynamicCalculator;
257         }
258         
259         public Configuration getConfiguration() {
260                 return configuration;
261         }
262
263     /**
264      * Get the center of pressure figure element.
265      * 
266      * @return center of pressure info
267      */
268     public Caret getExtraCP () {
269         return extraCP;
270     }
271
272     /**
273      * Get the center of gravity figure element.
274      * 
275      * @return center of gravity info
276      */
277     public Caret getExtraCG () {
278         return extraCG;
279     }
280
281     /**
282      * Get the extra text figure element.
283      * 
284      * @return extra text that contains info about the rocket design
285      */
286     public RocketInfo getExtraText () {
287         return extraText;
288     }
289
290     public void setSelectionModel(TreeSelectionModel m) {
291                 if (selectionModel != null) {
292                         selectionModel.removeTreeSelectionListener(this);
293                 }
294                 selectionModel = m;
295                 selectionModel.addTreeSelectionListener(this);
296                 valueChanged((TreeSelectionEvent) null); // updates FigureParameters
297         }
298         
299         
300
301         /**
302          * Return the angle of attack used in CP calculation.  NaN signifies the default value
303          * of zero.
304          * @return   the angle of attack used, or NaN.
305          */
306         public double getCPAOA() {
307                 return cpAOA;
308         }
309         
310         /**
311          * Set the angle of attack to be used in CP calculation.  A value of NaN signifies that
312          * the default AOA (zero) should be used.
313          * @param aoa   the angle of attack to use, or NaN
314          */
315         public void setCPAOA(double aoa) {
316                 if (MathUtil.equals(aoa, cpAOA) ||
317                                 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
318                         return;
319                 cpAOA = aoa;
320                 updateExtras();
321                 figure.updateFigure();
322                 fireChangeEvent();
323         }
324         
325         public double getCPTheta() {
326                 return cpTheta;
327         }
328         
329         public void setCPTheta(double theta) {
330                 if (MathUtil.equals(theta, cpTheta) ||
331                                 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
332                         return;
333                 cpTheta = theta;
334                 if (!Double.isNaN(theta))
335                         figure.setRotation(theta);
336                 updateExtras();
337                 figure.updateFigure();
338                 fireChangeEvent();
339         }
340         
341         public double getCPMach() {
342                 return cpMach;
343         }
344         
345         public void setCPMach(double mach) {
346                 if (MathUtil.equals(mach, cpMach) ||
347                                 (Double.isNaN(mach) && Double.isNaN(cpMach)))
348                         return;
349                 cpMach = mach;
350                 updateExtras();
351                 figure.updateFigure();
352                 fireChangeEvent();
353         }
354         
355         public double getCPRoll() {
356                 return cpRoll;
357         }
358         
359         public void setCPRoll(double roll) {
360                 if (MathUtil.equals(roll, cpRoll) ||
361                                 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
362                         return;
363                 cpRoll = roll;
364                 updateExtras();
365                 figure.updateFigure();
366                 fireChangeEvent();
367         }
368         
369         
370
371         @Override
372         public void addChangeListener(ChangeListener listener) {
373                 listeners.add(0, listener);
374         }
375         
376         @Override
377         public void removeChangeListener(ChangeListener listener) {
378                 listeners.remove(listener);
379         }
380         
381         protected void fireChangeEvent() {
382                 ChangeEvent e = new ChangeEvent(this);
383                 ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
384                 for (ChangeListener l : list) {
385                         l.stateChanged(e);
386                 }
387         }
388         
389         
390
391
392         /**
393          * Handle clicking on figure shapes.  The functioning is the following:
394          * 
395          * Get the components clicked.
396          * If no component is clicked, do nothing.
397          * If the currently selected component is in the set, keep it, 
398          * unless the selector specified is pressed.  If it is pressed, cycle to 
399          * the next component. Otherwise select the first component in the list. 
400          */
401         public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
402         
403         private void handleMouseClick(MouseEvent event) {
404                 if (event.getButton() != MouseEvent.BUTTON1)
405                         return;
406                 Point p0 = event.getPoint();
407                 Point p1 = scrollPane.getViewport().getViewPosition();
408                 int x = p0.x + p1.x;
409                 int y = p0.y + p1.y;
410                 
411                 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
412                 
413                 // If no component is clicked, do nothing
414                 if (clicked.length == 0)
415                         return;
416                 
417                 // Check whether the currently selected component is in the clicked components.
418                 TreePath path = selectionModel.getSelectionPath();
419                 if (path != null) {
420                         RocketComponent current = (RocketComponent) path.getLastPathComponent();
421                         path = null;
422                         for (int i = 0; i < clicked.length; i++) {
423                                 if (clicked[i] == current) {
424                                         if (event.isShiftDown() && (event.getClickCount() == 1)) {
425                                                 path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]);
426                                         } else {
427                                                 path = ComponentTreeModel.makeTreePath(clicked[i]);
428                                         }
429                                         break;
430                                 }
431                         }
432                 }
433                 
434                 // Currently selected component not clicked
435                 if (path == null) {
436                         if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
437                                 path = ComponentTreeModel.makeTreePath(clicked[1]);
438                         } else {
439                                 path = ComponentTreeModel.makeTreePath(clicked[0]);
440                         }
441                 }
442                 
443                 // Set selection and check for double-click
444                 selectionModel.setSelectionPath(path);
445                 if (event.getClickCount() == 2) {
446                         RocketComponent component = (RocketComponent) path.getLastPathComponent();
447                         
448                         ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
449                                         document, component);
450                 }
451         }
452         
453         
454
455
456         /**
457          * Updates the extra data included in the figure.  Currently this includes
458          * the CP and CG carets.
459          */
460         private WarningSet warnings = new WarningSet();
461         
462         private void updateExtras() {
463                 Coordinate cp, cg;
464                 double cpx, cgx;
465                 
466                 // TODO: MEDIUM: User-definable conditions
467                 FlightConditions conditions = new FlightConditions(configuration);
468                 warnings.clear();
469                 
470                 if (!Double.isNaN(cpMach)) {
471                         conditions.setMach(cpMach);
472                         extraText.setMach(cpMach);
473                 } else {
474                         conditions.setMach(Prefs.getDefaultMach());
475                         extraText.setMach(Prefs.getDefaultMach());
476                 }
477                 
478                 if (!Double.isNaN(cpAOA)) {
479                         conditions.setAOA(cpAOA);
480                 } else {
481                         conditions.setAOA(0);
482                 }
483                 extraText.setAOA(cpAOA);
484                 
485                 if (!Double.isNaN(cpRoll)) {
486                         conditions.setRollRate(cpRoll);
487                 } else {
488                         conditions.setRollRate(0);
489                 }
490                 
491                 if (!Double.isNaN(cpTheta)) {
492                         conditions.setTheta(cpTheta);
493                         cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
494                 } else {
495                         cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
496                 }
497                 extraText.setTheta(cpTheta);
498                 
499
500                 cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
501                 //              System.out.println("CG computed as "+cg+ " CP as "+cp);
502                 
503                 if (cp.weight > 0.000001)
504                         cpx = cp.x;
505                 else
506                         cpx = Double.NaN;
507                 
508                 if (cg.weight > 0.000001)
509                         cgx = cg.x;
510                 else
511                         cgx = Double.NaN;
512                 
513                 // Length bound is assumed to be tight
514                 double length = 0, diameter = 0;
515                 Collection<Coordinate> bounds = configuration.getBounds();
516                 if (!bounds.isEmpty()) {
517                         double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
518                         for (Coordinate c : bounds) {
519                                 if (c.x < minX)
520                                         minX = c.x;
521                                 if (c.x > maxX)
522                                         maxX = c.x;
523                         }
524                         length = maxX - minX;
525                 }
526                 
527                 for (RocketComponent c : configuration) {
528                         if (c instanceof SymmetricComponent) {
529                                 double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
530                                 double d2 = ((SymmetricComponent) c).getAftRadius() * 2;
531                                 diameter = MathUtil.max(diameter, d1, d2);
532                         }
533                 }
534                 
535                 extraText.setCG(cgx);
536                 extraText.setCP(cpx);
537                 extraText.setLength(length);
538                 extraText.setDiameter(diameter);
539                 extraText.setMass(cg.weight);
540                 extraText.setWarnings(warnings);
541                 
542
543                 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
544                         
545                         // TODO: LOW: Y-coordinate and rotation
546                         extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
547                         extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
548                         
549                 } else {
550                         
551                         extraCP.setPosition(Double.NaN, Double.NaN);
552                         extraCG.setPosition(Double.NaN, Double.NaN);
553                         
554                 }
555                 
556
557                 ////////  Flight simulation in background
558                 
559                 // Check whether to compute or not
560                 if (!Prefs.computeFlightInBackground()) {
561                         extraText.setFlightData(null);
562                         extraText.setCalculatingData(false);
563                         stopBackgroundSimulation();
564                         return;
565                 }
566                 
567                 // Check whether data is already up to date
568                 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
569                                 flightDataMotorID == configuration.getMotorConfigurationID()) {
570                         return;
571                 }
572                 
573                 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
574                 flightDataMotorID = configuration.getMotorConfigurationID();
575                 
576                 // Stop previous computation (if any)
577                 stopBackgroundSimulation();
578                 
579                 // Check that configuration has motors
580                 if (!configuration.hasMotors()) {
581                         extraText.setFlightData(FlightData.NaN_DATA);
582                         extraText.setCalculatingData(false);
583                         return;
584                 }
585                 
586                 // Start calculation process
587                 extraText.setCalculatingData(true);
588                 
589                 Rocket duplicate = (Rocket) configuration.getRocket().copy();
590                 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
591                 simulation.getConditions().setMotorConfigurationID(
592                                 configuration.getMotorConfigurationID());
593                 
594                 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
595                 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
596         }
597         
598         /**
599          * Cancels the current background simulation worker, if any.
600          */
601         private void stopBackgroundSimulation() {
602                 if (backgroundSimulationWorker != null) {
603                         backgroundSimulationWorker.cancel(true);
604                         backgroundSimulationWorker = null;
605                 }
606         }
607         
608         
609         /**
610          * A SimulationWorker that simulates the rocket flight in the background and
611          * sets the results to the extra text when finished.  The worker can be cancelled
612          * if necessary.
613          */
614         private class BackgroundSimulationWorker extends SimulationWorker {
615                 
616                 public BackgroundSimulationWorker(Simulation sim) {
617                         super(sim);
618                 }
619                 
620                 @Override
621                 protected FlightData doInBackground() {
622                         
623                         // Pause a little while to allow faster UI reaction
624                         try {
625                                 Thread.sleep(300);
626                         } catch (InterruptedException ignore) {
627                         }
628                         if (isCancelled() || backgroundSimulationWorker != this)
629                                 return null;
630                         
631                         return super.doInBackground();
632                 }
633                 
634                 @Override
635                 protected void simulationDone() {
636                         // Do nothing if cancelled
637                         if (isCancelled() || backgroundSimulationWorker != this)
638                                 return;
639                         
640                         backgroundSimulationWorker = null;
641                         extraText.setFlightData(simulation.getSimulatedData());
642                         extraText.setCalculatingData(false);
643                         figure.repaint();
644                 }
645                 
646                 @Override
647                 protected SimulationListener[] getExtraListeners() {
648                         return new SimulationListener[] {
649                                         InterruptListener.INSTANCE,
650                                         ApogeeEndListener.INSTANCE };
651                 }
652                 
653                 @Override
654                 protected void simulationInterrupted(Throwable t) {
655                         // Do nothing on cancel, set N/A data otherwise
656                         if (isCancelled() || backgroundSimulationWorker != this) // Double-check
657                                 return;
658                         
659                         backgroundSimulationWorker = null;
660                         extraText.setFlightData(FlightData.NaN_DATA);
661                         extraText.setCalculatingData(false);
662                         figure.repaint();
663                 }
664         }
665         
666         
667
668         /**
669          * Adds the extra data to the figure.  Currently this includes the CP and CG carets.
670          */
671         private void addExtras() {
672                 figure.clearRelativeExtra();
673                 extraCG = new CGCaret(0, 0);
674                 extraCP = new CPCaret(0, 0);
675                 extraText = new RocketInfo(configuration);
676                 updateExtras();
677                 figure.addRelativeExtra(extraCP);
678                 figure.addRelativeExtra(extraCG);
679                 figure.addAbsoluteExtra(extraText);
680         }
681         
682         
683         /**
684          * Updates the selection in the FigureParameters and repaints the figure.  
685          * Ignores the event itself.
686          */
687         @Override
688         public void valueChanged(TreeSelectionEvent e) {
689                 TreePath[] paths = selectionModel.getSelectionPaths();
690                 if (paths == null) {
691                         figure.setSelection(null);
692                         return;
693                 }
694                 
695                 RocketComponent[] components = new RocketComponent[paths.length];
696                 for (int i = 0; i < paths.length; i++)
697                         components[i] = (RocketComponent) paths[i].getLastPathComponent();
698                 figure.setSelection(components);
699         }
700         
701         
702
703         /**
704          * An <code>Action</code> that shows whether the figure type is the type
705          * given in the constructor.
706          * 
707          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
708          */
709         private class FigureTypeAction extends AbstractAction implements ChangeListener {
710                 private final int type;
711                 
712                 public FigureTypeAction(int type) {
713                         this.type = type;
714                         stateChanged(null);
715                         figure.addChangeListener(this);
716                 }
717                 
718                 @Override
719                 public void actionPerformed(ActionEvent e) {
720                         boolean state = (Boolean) getValue(Action.SELECTED_KEY);
721                         if (state == true) {
722                                 // This view has been selected
723                                 figure.setType(type);
724                                 updateExtras();
725                         }
726                         stateChanged(null);
727                 }
728                 
729                 @Override
730                 public void stateChanged(ChangeEvent e) {
731                         putValue(Action.SELECTED_KEY, figure.getType() == type);
732                 }
733         }
734         
735 }