001    /*
002     * $URL: file:///mit/6.170/groups/se64/repository/trunk/gizmoball/Display.java $
003     * $Id: Display.java 691 2007-05-14 23:50:08Z billmag $
004     */
005    
006    package gizmoball;
007    
008    import gizmoball.physics3d.Vect3;
009    
010    import javax.media.opengl.GLCanvas;
011    import javax.media.opengl.GLJPanel;
012    
013    import java.awt.Container;
014    import java.awt.Dimension;
015    import java.awt.event.ActionEvent;
016    import java.awt.event.ActionListener;
017    import java.awt.event.ItemEvent;
018    import java.awt.event.ItemListener;
019    import java.io.File;
020    import java.net.MalformedURLException;
021    import java.net.URL;
022    import java.util.SortedSet;
023    import java.util.TreeSet;
024    import java.util.Vector;
025    
026    import javax.swing.JButton;
027    import javax.swing.JComboBox;
028    import javax.swing.JDialog;
029    import javax.swing.JFileChooser;
030    import javax.swing.JFrame;
031    import javax.swing.JLabel;
032    import javax.swing.JMenu;
033    import javax.swing.JMenuBar;
034    import javax.swing.JMenuItem;
035    import javax.swing.JOptionPane;
036    import javax.swing.JPanel;
037    import javax.swing.JTabbedPane;
038    import javax.swing.Spring;
039    import javax.swing.SpringLayout;
040    import javax.swing.SwingUtilities;
041    import javax.swing.event.ChangeEvent;
042    import javax.swing.event.ChangeListener;
043    import javax.swing.event.MenuEvent;
044    import javax.swing.event.MenuListener;
045    
046    
047    /**
048     * Main window and controller of the Gizmoball application.
049     * <code>Display</code> manages all of the state of the Gizmoball
050     * application, both the instances of lower-level modules and the Swing
051     * graphical user interface presented to the user.
052     * <p>
053     * Beware that because one of <code>Display</code>'s major tasks is
054     * interacting with the Swing graphical user interface of the Gizmoball
055     * application, care must be taken with respect to threading. In
056     * particular, unless otherwise specified, all methods in this class
057     * should be called only from the Swing event dispatch thread. If a
058     * client not in the event dispatch thread wants to call a method on a
059     * <code>Display</code>, it should use {@link
060     * SwingUtilities#invokeAndWait} or {@link SwingUtilities#invokeLater}.
061     * <p>
062     * For those familiar with the model-view-controller design pattern,
063     * <code>Display</code> represents the controller in the overall
064     * architecture of the Gizmoball application. The <code>Board</code> and
065     * the <code>Gizmo</code> classes represent most of the model, and the
066     * <code>BoardView</code> and <code>GizmoView</code> classes represent
067     * the view.
068     *
069     * @specfield mode : <code>Display.Mode</code> -
070     *     the current mode this <code>Display</code> is in
071     * @specfield board : <code>Board</code> -
072     *     the currently loaded <code>Board</code>
073     * @specfield boardFile : <code>File</code> -
074     *     the file <code>board</code> was loaded from or last saved to
075     * @specfield clockTickSpeed : integer -
076     *     the number of milliseconds between ticks of the
077     *     <code>Clock</code> in running mode
078     * @specfield isClockRunning : boolean -
079     *     whether the <code>Clock</code> is currently running
080     */
081    public final class Display extends JFrame {
082    
083        // Abstraction Function:
084        // TODO: Define mode.
085        // abstract.board = concrete.board;
086        // abstract.clockTickSpeed = concrete.clockTickSpeed;
087        // abstract.isClockRunning = concrete.clock.isRunning()
088    
089        // Representation Invariant:
090        // clockTickSpeed > 0
091        // boardView != null
092        // if board != null, then clock != null
093    
094    
095        /*------------------------------------------------------------------
096         * Fields
097         *----------------------------------------------------------------*/
098    
099        // The currently loaded Board.
100        private Board board;
101    
102        // The file board was loaded from or last saved to
103        private File boardFile;
104    
105        // The number of milliseconds between ticks of the Clock in running
106        // mode.
107        private int clockTickSpeed;
108    
109        // The currently active Clock.
110        private Clock clock;
111    
112        // The BoardView that is in charge of displaying board.
113        private final GLBoardView boardView;
114    
115        // The SwingKeyboardInput that is in charge of displaying board.
116        private SwingKeyboardInput keyboardInput;
117    
118        // The GLCanvas boardView renders in
119        private final GLCanvas boardCanvas;
120    
121        // The tabbed pane containing the right bar controls
122        private final JTabbedPane rightBar;
123    
124        // The panel containing the right bar controls for running mode
125        private final RunModePanel runModePanel;
126    
127        // The panel containing the right bar controls for building mode
128        private final BuildModePanel buildModePanel;
129    
130        // The top-level menu in running mode
131        private final JMenuBar runModeMenu;
132    
133        // The top-level menu in buildling mode
134        private final JMenuBar buildModeMenu;
135    
136        // A MenuListener that pauses the Clock when the menu is opened
137        private final MenuListener pausingMenuListener =
138            new MenuListener() {
139                public void menuCanceled(MenuEvent e) { }
140                public void menuDeselected(MenuEvent e) { }
141                public void menuSelected(MenuEvent e) { stopTime(); }
142            };
143    
144        // A JFileChooser for the load and save as dialogs
145        private final JFileChooser fileChooser = new JFileChooser();
146    
147        // The Gizmo being manipulated currently in building mode
148        private Gizmo currentGizmo = null;
149    
150        // XXX: To quench a compiler warning; should be set to better value
151        // later.
152        private static final long serialVersionUID = 1L;
153    
154        // Debugging output level. Follows the semantics defined by
155        // DebugUtils.printMessage.
156        private static final int debug = 0;
157    
158        // Whether to run expensive checkRep() tests.
159        private static final boolean runExpensiveCheckRepTests = false;
160    
161        /**
162         * Class revision identifier. <code>rcsid</code> contains an RCS Id
163         * keyword string that may be used to identify which revision of
164         * <code>Display.java</code> was used to generate an instance of
165         * <code>Display.class</code>. To identify the revision of a
166         * <code>Display.class</code> file, run "<code>ident
167         * Display.class</code>" (<code>ident</code> is part of the <a
168         * href="http://www.cs.purdue.edu/homes/trinkle/RCS/">RCS</a>
169         * software package).
170         */
171        public static final String rcsid = "$Id: Display.java 691 2007-05-14 23:50:08Z billmag $";
172    
173    
174        /*------------------------------------------------------------------
175         * Constructors
176         *----------------------------------------------------------------*/
177    
178        /**
179         * Constructs a new <code>Display</code>. The resulting
180         * <code>Display</code> is in <code>Mode.RUN_MODE</code>, with no
181         * currently loaded board, and with time paused. The default time
182         * between <code>Clock</code> ticks is set to 40 milliseconds,
183         * equivalent to 25 ticks per second.
184         * <p>
185         * This method only prepares a <code>Display</code> for use. It
186         * populates the window with controls and hooks up the correct event
187         * handlers, but it does not show the window. To show the window,
188         * call <code>setVisible(true)</code> on this <code>Display</code>.
189         *
190         * @effects <code>mode = Mode.RUN_MODE</code>
191         * @effects <code>board = null</code>
192         * @effects <code>clockTickSpeed</code> = 40 milliseconds
193         * @effects <code>isClockRunning = false</code>
194         */
195        public Display() {
196            super("Gadgix Gizmoball");
197    
198            assert SwingUtilities.isEventDispatchThread();
199    
200            // Top-level window content pane -------------------------------
201    
202            Container contentPane = getContentPane();
203            SpringLayout layout = new SpringLayout();
204            contentPane.setLayout(layout);
205    
206            SpringLayout.Constraints contentPaneCons =
207                layout.getConstraints(contentPane);
208    
209    
210            // Display of the Board ----------------------------------------
211    
212            boardCanvas = new GLCanvas();
213            boardCanvas.setPreferredSize(new Dimension(600, 600));
214            boardCanvas.setFocusable(true);
215            boardView = new GLBoardView(boardCanvas);
216    
217            contentPane.add(boardCanvas);
218            SpringLayout.Constraints boardCanvasCons =
219                layout.getConstraints(boardCanvas); 
220    
221            // Enforce left and top margins
222            boardCanvasCons.setX(contentPaneCons.getX());
223            boardCanvasCons.setY(contentPaneCons.getY());
224    
225            // Set that we want to calculate the default window height based
226            // on the height of the board display
227            contentPaneCons.setConstraint(SpringLayout.SOUTH,
228                boardCanvasCons.getConstraint(SpringLayout.SOUTH));
229    
230    
231            // Tabbed pane on the right ------------------------------------
232    
233            rightBar = new JTabbedPane();
234    
235            contentPane.add(rightBar);
236            SpringLayout.Constraints rightBarCons =
237                layout.getConstraints(rightBar);
238    
239            // Place just to the right of the Board display
240            rightBarCons.setX(boardCanvasCons.getConstraint(SpringLayout.EAST));
241    
242            // Snap top and bottom to top and bottom of Board display
243            rightBarCons.setY(boardCanvasCons.getY());
244            rightBarCons.setHeight(boardCanvasCons.getHeight());
245    
246            // Set that we want to calculate the default window width based
247            // on the width of the TabbedPane
248            contentPaneCons.setConstraint(SpringLayout.EAST,
249                rightBarCons.getConstraint(SpringLayout.EAST));
250    
251            // Catch when the user tries to switch tabs
252            rightBar.addChangeListener(new ChangeListener() {
253                public void stateChanged(ChangeEvent e) {
254                    if (rightBar.getSelectedIndex() == 0) {
255                        setMode(Mode.RUN_MODE);
256                    } else {
257                        assert rightBar.getSelectedIndex() == 1;
258                        setMode(Mode.BUILD_MODE);
259                    }
260                }
261            });
262    
263            // Tabbed pane contents ----------------------------------------
264    
265            runModePanel = new RunModePanel();
266            rightBar.addTab("Running Mode", runModePanel);
267    
268            buildModePanel = new BuildModePanel();
269            rightBar.addTab("Building Mode", buildModePanel);
270    
271    
272            // Menus -------------------------------------------------------
273    
274            runModeMenu = new JMenuBar();
275            runModeMenu.add(buildBoardMenu());
276            runModeMenu.add(buildHelpMenu());
277    
278            buildModeMenu = new JMenuBar();
279            buildModeMenu.add(buildBoardMenu());
280            buildModeMenu.add(buildAddGizmoMenu());
281            buildModeMenu.add(buildConnectionsMenu());
282            buildModeMenu.add(buildHelpMenu());
283    
284            setJMenuBar(runModeMenu);
285    
286    
287            // Miscellaneous -----------------------------------------------
288    
289            // Actually calculate and set the window width and height
290            pack();
291    
292            // Center the window
293            setLocationRelativeTo(null);
294    
295            // TODO: Should eventually be hooked to something that checks
296            // for unsaved changes in the Board before exiting.
297            setDefaultCloseOperation(EXIT_ON_CLOSE);
298    
299            clockTickSpeed = 30;
300    
301            assert checkRep();
302        }
303    
304        // Builds the Board menu.
305        private JMenu buildBoardMenu() {
306            JMenu menu = new JMenu("Board");
307    
308            // XXX: Needed because we're using the heavyweight GLCanvas
309            // instead of the lightweight GLJPanel.
310            menu.getPopupMenu().setLightWeightPopupEnabled(false);
311    
312            menu.addMenuListener(pausingMenuListener);
313    
314            JMenuItem menuItem;
315    
316            menuItem = new JMenuItem("New");
317            menuItem.addActionListener(new ActionListener() {
318                public void actionPerformed(ActionEvent e) {
319                    newBoard();
320                }
321            });
322            menu.add(menuItem);
323    
324            menuItem = new JMenuItem("Open...");
325            menuItem.addActionListener(new ActionListener() {
326                public void actionPerformed(ActionEvent e) {
327                    openFromBoardMenu();
328                }
329            });
330            menu.add(menuItem);
331    
332            menu.addSeparator();
333    
334            menuItem = new JMenuItem("Save");
335            menuItem.addActionListener(new ActionListener() {
336                public void actionPerformed(ActionEvent e) {
337                    saveFromBoardMenu();
338                }
339            });
340            menu.add(menuItem);
341    
342            menuItem = new JMenuItem("Save As...");
343            menuItem.addActionListener(new ActionListener() {
344                public void actionPerformed(ActionEvent e) {
345                    saveAsFromBoardMenu();
346                }
347            });
348            menu.add(menuItem);
349    
350            menu.addSeparator();
351    
352            menuItem = new JMenuItem("Quit");
353            menuItem.addActionListener(new ActionListener() {
354                public void actionPerformed(ActionEvent e) {
355                    // TODO: Should eventually be hooked to something that
356                    // checks for unsaved changes in the Board before
357                    // exiting.
358                    System.exit(0);
359                }
360            });
361            menu.add(menuItem);
362    
363            return menu;
364        }
365    
366        // Builds the Add Gizmo menu.
367        private JMenu buildAddGizmoMenu() {
368            JMenu menu = new JMenu("Add Gizmo");
369    
370            // XXX: Needed because we're using the heavyweight GLCanvas
371            // instead of the lightweight GLJPanel.
372            menu.getPopupMenu().setLightWeightPopupEnabled(false);
373    
374            menu.addMenuListener(pausingMenuListener);
375    
376            JMenuItem menuItem;
377    
378            menuItem = new JMenuItem("Add Square Bumper...");
379            menuItem.addActionListener(new ActionListener() {
380                public void actionPerformed(ActionEvent e) {
381                    addSquareBumperToBoard();
382                }
383            });
384            menu.add(menuItem);
385    
386            menuItem = new JMenuItem("Add Circular Bumper...");
387            menuItem.addActionListener(new ActionListener() {
388                public void actionPerformed(ActionEvent e) {
389                    addCircularBumperToBoard();
390                }
391            });
392            menu.add(menuItem);
393    
394            menuItem = new JMenuItem("Add Triangular Bumper...");
395            menuItem.addActionListener(new ActionListener() {
396                public void actionPerformed(ActionEvent e) {
397                    addTriangularBumperToBoard();
398                }
399            });
400            menu.add(menuItem);
401    
402            menu.addSeparator();
403    
404            menuItem = new JMenuItem("Add Left Flipper...");
405            menuItem.addActionListener(new ActionListener() {
406                public void actionPerformed(ActionEvent e) {
407                    addLeftFlipperToBoard();
408                }
409            });
410            menu.add(menuItem);
411    
412            menuItem = new JMenuItem("Add Right Flipper...");
413            menuItem.addActionListener(new ActionListener() {
414                public void actionPerformed(ActionEvent e) {
415                    addRightFlipperToBoard();
416                }
417            });
418            menu.add(menuItem);
419    
420            menu.addSeparator();
421    
422            menuItem = new JMenuItem("Add Absorber...");
423            menuItem.addActionListener(new ActionListener() {
424                public void actionPerformed(ActionEvent e) {
425                    addAbsorberToBoard();
426                }
427            });
428            menu.add(menuItem);
429    
430            menu.addSeparator();
431    
432            menuItem = new JMenuItem("Add Ball...");
433            menuItem.addActionListener(new ActionListener() {
434                public void actionPerformed(ActionEvent e) {
435                    addBallToBoard();
436                }
437            });
438            menu.add(menuItem);
439    
440            return menu;
441        }
442    
443        // Builds the Connections menu.
444        private JMenu buildConnectionsMenu() {
445            JMenu menu = new JMenu("Connections");
446    
447            // XXX: Needed because we're using the heavyweight GLCanvas
448            // instead of the lightweight GLJPanel.
449            menu.getPopupMenu().setLightWeightPopupEnabled(false);
450    
451            menu.addMenuListener(pausingMenuListener);
452    
453            JMenuItem menuItem;
454    
455            menuItem = new JMenuItem("Edit Gizmo-Gizmo Connections...");
456            menuItem.addActionListener(new ActionListener() {
457                public void actionPerformed(ActionEvent e) {
458                    gizmoConnectModify();
459                }
460            });
461            menu.add(menuItem);
462    
463            menuItem = new JMenuItem("Edit Keyboard Connections...");
464            menuItem.addActionListener(new ActionListener() {
465                public void actionPerformed(ActionEvent e) {
466                    keyConnectModify();
467                }
468            });
469            menu.add(menuItem);
470    
471            return menu;
472        }
473    
474        // Builds the Help menu.
475        private JMenu buildHelpMenu() {
476            JMenu menu = new JMenu("Help");
477    
478            // XXX: Needed because we're using the heavyweight GLCanvas
479            // instead of the lightweight GLJPanel.
480            menu.getPopupMenu().setLightWeightPopupEnabled(false);
481    
482            menu.addMenuListener(pausingMenuListener);
483    
484            JMenuItem menuItem;
485    
486            menuItem = new JMenuItem("User Guide (external web browser)");
487            menuItem.addActionListener(new ActionListener() {
488                public void actionPerformed(ActionEvent e) {
489                    userGuideFromHelpMenu();
490                }
491            });
492            menu.add(menuItem);
493    
494            menuItem = new JMenuItem("About...");
495            menuItem.addActionListener(new ActionListener() {
496                public void actionPerformed(ActionEvent e) {
497                    aboutFromHelpMenu();
498                }
499            });
500            menu.add(menuItem);
501    
502            return menu;
503        }
504    
505    
506        /*------------------------------------------------------------------
507         * checkRep()
508         *----------------------------------------------------------------*/
509    
510        // Checks that the representation invariant has not been violated.
511        // Returns true if the representation invariant holds. Throws a
512        // RuntimeException if the representation invariant does not hold.
513        private boolean checkRep() {
514            // TODO: Implement me.
515    
516            return true;
517        }
518    
519    
520        /*------------------------------------------------------------------
521         * Observers
522         *----------------------------------------------------------------*/
523    
524        /**
525         * Returns the current mode. Returns whether we're in running mode
526         * or building mode.
527         *
528         * @return <code>this.mode</code>
529         */
530        public Mode getMode() {
531            assert checkRep();
532            assert SwingUtilities.isEventDispatchThread();
533    
534            Mode mode;
535    
536            if (rightBar.getSelectedIndex() == 0) {
537                mode = Mode.RUN_MODE;
538            } else {
539                assert rightBar.getSelectedIndex() == 1;
540                mode = Mode.BUILD_MODE;
541            }
542    
543            assert checkRep();
544            return mode;
545        }
546    
547        /*------------------------------------------------------------------
548         * Mutators
549         *----------------------------------------------------------------*/
550    
551        /**
552         * Loads a <code>Board</code> from an XML file stored at the given
553         * URL. If an error occurred in loading the XML file, a dialog
554         * message is displayed, and the current <code>Board</code> is not
555         * changed.
556         *
557         * @param url the URL at which to find the XML file containing the
558         *            new <code>Board</code>
559         * @requires <code>url != null</code>
560         * @effects <code>this.board<sub>post</sub></code> = a
561         *          <code>Board</code> loaded from <code>url</code> if the
562         *          loading was successful,
563         *          <code>this.board<sub>pre</sub></code> otherwise
564         * @effects <code>this.boardFile</code> is unset if the loading was
565         *          successful, because can't pull a useful
566         *          <code>File</code> from a <code>URL</code>
567         * @return <code>true</code> if the loading was successful,
568         *         <code>false</code> otherwise
569         */
570        public boolean loadBoardFromURL(URL url) {
571            assert checkRep();
572    
573            if (url == null) {
574                throw new NullPointerException("null was passed as argument url");
575            }
576    
577            boolean loadSucceeded = true;
578    
579            // FIXME: Stop clock? or specify that this must be called with a
580            // stopped clock?
581    
582            // TODO: Do this in a separate thread...
583    
584            // FIXME: Use new XMLLoadException once that's supported...
585            Board newBoard = null;
586            SwingKeyboardInput input = new SwingKeyboardInput();
587            keyboardInput = input;
588            boardCanvas.addKeyListener(input.getKeyListener());
589            try {
590                BoardBuilderXML builder =
591                    new BoardBuilderXML(new PreliminaryReleaseErrorHandler());
592                newBoard = builder.buildBoardFromFile(url, input);
593            } catch (XMLLoadException e) {
594                loadSucceeded = false;
595                e.printStackTrace();
596            }
597    
598            if (loadSucceeded) {
599                board = newBoard;
600                board.setDisplayMode(this);
601                board.setInputMode(input);
602                boardView.setBoard(board);
603                clock = new JavaUtilTimerClock(board, clockTickSpeed);
604                clock.addBoardView(boardView);
605                buildModePanel.refreshGizmosCombo();
606            } else {
607                JOptionPane.showMessageDialog(this,
608                    "An error occurred in loading " + url + ".", "File Error",
609                    JOptionPane.ERROR_MESSAGE);
610            }
611    
612            assert checkRep();
613            return loadSucceeded;
614        }
615    
616        /**
617         * Loads a <code>Board</code> from an XML file stored at the given
618         * <code>File</code>. If an error occurred in loading the XML file,
619         * a dialog message is displayed, and the current <code>Board</code>
620         * is not changed.
621         *
622         * @param file the <code>File</code> in which to find the XML file
623         *             containing the new <code>Board</code>
624         * @requires <code>file != null</code>
625         * @effects <code>this.board<sub>post</sub></code> = a
626         *          <code>Board</code> loaded from <code>file</code> if the
627         *          loading was successful,
628         *          <code>this.board<sub>pre</sub></code> otherwise
629         * @effects <code>this.boardFile</code> is set to <code>file</code>
630         *          if the loading was successful, left alone if not
631         * @return <code>true</code> if the loading was successful,
632         *         <code>false</code> otherwise
633         */
634        public boolean loadBoardFromFile(File file) {
635            assert checkRep();
636            assert SwingUtilities.isEventDispatchThread();
637    
638            URL url;
639            try {
640                url = file.toURI().toURL();
641            } catch (MalformedURLException e) {
642                e.printStackTrace();
643                JOptionPane.showMessageDialog(this,
644                    "An error occurred resolving " + file + ".", "File Error",
645                    JOptionPane.ERROR_MESSAGE);
646                assert checkRep();
647                return false;
648            }
649    
650            boolean loadSucceeded = loadBoardFromURL(url);
651    
652            if (loadSucceeded) {
653                boardFile = file;
654            }
655    
656            assert checkRep();
657            return loadSucceeded;
658        }
659    
660        /**
661         * Switches the mode. Changes the UI such that it's ready for a new
662         * mode.
663         *
664         * @param newMode new mode to switch to
665         * @requires <code>newMode != null</code>
666         * @effects <code>mode = newMode</code>
667         * @effects if <code>newMode = Mode.BUILD_MODE</code>,
668         *          then <code>isClockRunning = false</code>
669         */
670        public void setMode(Mode newMode) {
671            assert checkRep();
672            assert SwingUtilities.isEventDispatchThread();
673    
674            if (newMode == null) {
675                throw new NullPointerException("null passed as argument newMode");
676            }
677    
678            if (newMode == Mode.RUN_MODE) {
679                try {
680                    if (currentGizmo != null) {
681                        board.addGizmo(currentGizmo);
682                    }
683                } catch (NonUniqueGizmoNameException e) {
684                    // Cry
685                    System.err.println("Unexpected error!");
686                    e.printStackTrace();
687                }
688                currentGizmo = null;
689                if (board != null) {
690                    boardView.setBoard(board);
691                }
692                boardView.setSelectedGizmo(null);
693                rightBar.setSelectedIndex(0);
694                setJMenuBar(runModeMenu);
695                boardView.redraw();
696            } else {
697                assert newMode == Mode.BUILD_MODE;
698    
699                stopTime();
700                rightBar.setSelectedIndex(1);
701                setJMenuBar(buildModeMenu);
702                currentGizmo = board.checkoutGizmo(
703                    (String)buildModePanel.gizmosCombo.getSelectedItem());
704                boardView.setSelectedGizmo(currentGizmo);
705                boardView.setBoard(board);
706                boardView.redraw();
707            }
708    
709            assert checkRep();
710        }
711    
712    
713        /*------------------------------------------------------------------
714         * Running mode buttons handlers
715         *----------------------------------------------------------------*/
716    
717        /**
718         * Starts the <code>Clock</code>. Calling this method switches to
719         * running mode if this <code>Display</code> is not already in
720         * running mode. It then starts the <code>Clock</code> if the
721         * <code>Clock</code> is not already running and allows the
722         * <code>Board</code> simulation to proceed.
723         *
724         * @effects <code>mode = Mode.RUN_MODE</code>
725         * @effects <code>isClockRunning = true</code>
726         */
727        public void startTime() {
728            assert checkRep();
729            assert SwingUtilities.isEventDispatchThread();
730    
731            if (getMode() != Mode.RUN_MODE) {
732                setMode(Mode.RUN_MODE);
733            }
734    
735            if (!clock.isRunning()) {
736                clock.start();
737                runModePanel.setClockRunning(true);
738    
739                // Set focus to boardCanvas so that key triggers will work
740                // by default
741                // FIXME: This works on OS X (at least) but not on Linux...
742                boardCanvas.requestFocusInWindow();
743            }
744    
745            assert checkRep();
746        }
747    
748        /**
749         * Stops the <code>Clock</code>. This method stops the
750         * <code>Clock</code> if the <code>Clock</code> is currently running
751         * and thus pauses the <code>Board</code> simulation.
752         *
753         * @effects <code>isClockRunning = false</code>
754         */
755        public void stopTime() {
756            assert checkRep();
757            assert SwingUtilities.isEventDispatchThread();
758    
759            if (clock.isRunning()) {
760                // Must use an auxiliary thread because clock.stop() might
761                // need to finish a redraw, and thus might need to access
762                // this Swing event dispatch thread
763                Thread auxThread = new Thread(new Runnable() {
764                    public void run() {
765                        clock.stop();
766                    }
767                });
768                auxThread.start();
769    
770                // XXX: The clock will not stop until some indeterminate
771                // time in the future...
772    
773                runModePanel.setClockRunning(false);
774            }
775    
776            assert checkRep();
777        }
778    
779        /**
780         * Resets the <code>Board</code>. This method sets the
781         * <code>Board</code> back to the state that it would be in if it
782         * had just been loaded from a file.
783         *
784         * @requires <code>mode = Mode.RUN_MODE</code> and
785         *           <code>isClockRunning = false</code>
786         * @effects <code>board</code> is reset
787         */
788        public void resetBoard() {
789            assert checkRep();
790            assert SwingUtilities.isEventDispatchThread();
791    
792            // TODO: Reset ball(s) position and velocity.
793    
794            // TODO: Clear all pending triggers.
795            if (boardFile == null)
796                newBoard();
797            else
798                loadBoardFromFile(boardFile);
799    
800            assert checkRep();
801        }
802    
803    
804        /*------------------------------------------------------------------
805         * Building mode button handlers
806         *----------------------------------------------------------------*/
807    
808        /**
809         * Prompts the user for a new name for the currently selected Gizmo.
810         */
811        public void renameCurrentGizmo() {
812            assert checkRep();
813            assert SwingUtilities.isEventDispatchThread();
814            assert getMode() == Mode.BUILD_MODE;
815    
816            String newName = JOptionPane.showInputDialog(this,
817                "What would you like to name this gizmo?", "Rename Gizmo",
818                JOptionPane.QUESTION_MESSAGE);
819            if (board.getNames().contains(newName)) {
820                JOptionPane.showMessageDialog(this,
821                    "There already is a gizmo named " + newName + ".",
822                    "Rename Error", JOptionPane.ERROR_MESSAGE);
823            } else {
824                System.out.println("We want to rename " + currentGizmo.getName() +
825                                   " to " + newName);
826            }
827    
828            assert checkRep();
829        }
830    
831        /**
832         * Moves the currently selected Gizmo up in building mode. This
833         * corresponds with a decrese in the y coordinate.
834         */
835        public void moveCurrentGizmoUp() {
836            assert checkRep();
837            assert SwingUtilities.isEventDispatchThread();
838            assert getMode() == Mode.BUILD_MODE;
839    
840            double step = (currentGizmo instanceof Ball) ? 0.05 : 1.0;
841            Vect3 originalPosition = currentGizmo.getPosition();
842            boolean newPositionFound = false;
843    
844            yPositionLoop:
845            while (currentGizmo.getPosition().y() >= step) {
846                currentGizmo.move(new Vect3(0, -step, 0));
847    
848                for (Gizmo gizmo : board) {
849                    if (!(gizmo instanceof OuterWalls) &&
850                            GizmoBoundCalculator.doOverlap(currentGizmo, gizmo)) {
851                        continue yPositionLoop;
852                    }
853                }
854    
855                // If we got here, then no overlaps found
856                newPositionFound = true;
857                break;
858            }
859    
860            if (newPositionFound) {
861                boardView.redraw();
862            } else {
863                currentGizmo.moveTo(originalPosition);
864            }
865    
866            assert checkRep();
867        }
868    
869        /**
870         * Moves the currently selected Gizmo left in building mode. This
871         * corresponds with a decrese in the x coordinate.
872         */
873        public void moveCurrentGizmoLeft() {
874            assert checkRep();
875            assert SwingUtilities.isEventDispatchThread();
876            assert getMode() == Mode.BUILD_MODE;
877    
878            double step = (currentGizmo instanceof Ball) ? 0.05 : 1.0;
879            Vect3 originalPosition = currentGizmo.getPosition();
880            boolean newPositionFound = false;
881    
882            xPositionLoop:
883            while (currentGizmo.getPosition().x() >= step) {
884                currentGizmo.move(new Vect3(-step, 0, 0));
885    
886                for (Gizmo gizmo : board) {
887                    if (!(gizmo instanceof OuterWalls) &&
888                            GizmoBoundCalculator.doOverlap(currentGizmo, gizmo)) {
889                        continue xPositionLoop;
890                    }
891                }
892    
893                // If we got here, then no overlaps found
894                newPositionFound = true;
895                break;
896            }
897    
898            if (newPositionFound) {
899                boardView.redraw();
900            } else {
901                currentGizmo.moveTo(originalPosition);
902            }
903    
904            assert checkRep();
905        }
906    
907        /**
908         * Moves the currently selected Gizmo right in building mode. This
909         * corresponds with an increase in the x coordinate.
910         */
911        public void moveCurrentGizmoRight() {
912            assert checkRep();
913            assert SwingUtilities.isEventDispatchThread();
914            assert getMode() == Mode.BUILD_MODE;
915    
916            double step = (currentGizmo instanceof Ball) ? 0.05 : 1.0;
917            Vect3 originalPosition = currentGizmo.getPosition();
918            boolean newPositionFound = false;
919    
920            xPositionLoop:
921            while ((currentGizmo instanceof Ball) ?
922                   (currentGizmo.getPosition().x() <= 20.0 - 0.25 - step) :
923                   (GizmoBoundCalculator.ceilX(currentGizmo) <= 20.0 - step)) {
924                currentGizmo.move(new Vect3(step, 0, 0));
925    
926                for (Gizmo gizmo : board) {
927                    if (!(gizmo instanceof OuterWalls) &&
928                            GizmoBoundCalculator.doOverlap(currentGizmo, gizmo)) {
929                        continue xPositionLoop;
930                    }
931                }
932    
933                // If we got here, then no overlaps found
934                newPositionFound = true;
935                break;
936            }
937    
938            if (newPositionFound) {
939                boardView.redraw();
940            } else {
941                currentGizmo.moveTo(originalPosition);
942            }
943    
944            assert checkRep();
945        }
946    
947        /**
948         * Moves the currently selected Gizmo down in building mode. This
949         * corresponds with an increase in the y coordinate.
950         */
951        public void moveCurrentGizmoDown() {
952            assert checkRep();
953            assert SwingUtilities.isEventDispatchThread();
954            assert getMode() == Mode.BUILD_MODE;
955    
956            double step = (currentGizmo instanceof Ball) ? 0.05 : 1.0;
957            Vect3 originalPosition = currentGizmo.getPosition();
958            boolean newPositionFound = false;
959    
960            yPositionLoop:
961            while ((currentGizmo instanceof Ball) ?
962                   (currentGizmo.getPosition().y() <= 20.0 - 0.25 - step) :
963                   (GizmoBoundCalculator.ceilY(currentGizmo) <= 20.0 - step)) {
964                currentGizmo.move(new Vect3(0, step, 0));
965    
966                for (Gizmo gizmo : board) {
967                    if (!(gizmo instanceof OuterWalls) &&
968                            GizmoBoundCalculator.doOverlap(currentGizmo, gizmo)) {
969                        continue yPositionLoop;
970                    }
971                }
972    
973                // If we got here, then no overlaps found
974                newPositionFound = true;
975                break;
976            }
977    
978            if (newPositionFound) {
979                boardView.redraw();
980            } else {
981                currentGizmo.moveTo(originalPosition);
982            }
983    
984            assert checkRep();
985        }
986    
987        /**
988         * Moves the currently selected Gizmo backward in building mode.
989         * This corresponds with an increase in the z coordinate.
990         */
991        public void moveCurrentGizmoBackward() {
992            assert checkRep();
993            assert SwingUtilities.isEventDispatchThread();
994            assert getMode() == Mode.BUILD_MODE;
995    
996            double step = (currentGizmo instanceof Ball) ? 0.05 : 1.0;
997            Vect3 originalPosition = currentGizmo.getPosition();
998            boolean newPositionFound = false;
999    
1000            zPositionLoop:
1001            while ((currentGizmo instanceof Ball) ?
1002                   (currentGizmo.getPosition().z() <= 10.0 - 0.25 - step) :
1003                   (GizmoBoundCalculator.ceilZ(currentGizmo) <= 10.0 - step)) {
1004                currentGizmo.move(new Vect3(0, 0, step));
1005    
1006                for (Gizmo gizmo : board) {
1007                    if (!(gizmo instanceof OuterWalls) &&
1008                            GizmoBoundCalculator.doOverlap(currentGizmo, gizmo)) {
1009                        continue zPositionLoop;
1010                    }
1011                }
1012    
1013                // If we got here, then no overlaps found
1014                newPositionFound = true;
1015                break;
1016            }
1017    
1018            if (newPositionFound) {
1019                boardView.redraw();
1020            } else {
1021                currentGizmo.moveTo(originalPosition);
1022            }
1023    
1024            assert checkRep();
1025        }
1026    
1027        /**
1028         * Moves the currently selected Gizmo forward in building mode. This
1029         * corresponds with a decrese in the z coordinate.
1030         */
1031        public void moveCurrentGizmoForward() {
1032            assert checkRep();
1033            assert SwingUtilities.isEventDispatchThread();
1034            assert getMode() == Mode.BUILD_MODE;
1035    
1036            double step = (currentGizmo instanceof Ball) ? 0.05 : 1.0;
1037            Vect3 originalPosition = currentGizmo.getPosition();
1038            boolean newPositionFound = false;
1039    
1040            zPositionLoop:
1041            while (currentGizmo.getPosition().z() >= step) {
1042                currentGizmo.move(new Vect3(0, 0, -step));
1043    
1044                for (Gizmo gizmo : board) {
1045                    if (!(gizmo instanceof OuterWalls) &&
1046                            GizmoBoundCalculator.doOverlap(currentGizmo, gizmo)) {
1047                        continue zPositionLoop;
1048                    }
1049                }
1050    
1051                // If we got here, then no overlaps found
1052                newPositionFound = true;
1053                break;
1054            }
1055    
1056            if (newPositionFound) {
1057                boardView.redraw();
1058            } else {
1059                currentGizmo.moveTo(originalPosition);
1060            }
1061    
1062            assert checkRep();
1063        }
1064    
1065        /**
1066         * Rotates the currently selected Gizmo clockwise by 90 degrees.
1067         */
1068        public void rotateCurrentGizmoClockwise() {
1069            assert checkRep();
1070            assert SwingUtilities.isEventDispatchThread();
1071            assert getMode() == Mode.BUILD_MODE;
1072    
1073            currentGizmo.setOrientation(
1074                (currentGizmo.getOrientation() - 90 + 360) % 360);
1075            boardView.redraw();
1076    
1077            assert checkRep();
1078        }
1079    
1080        /**
1081         * Rotates the currently selected Gizmo counterclockwise by 90
1082         * degrees.
1083         */
1084        public void rotateCurrentGizmoCounterclockwise() {
1085            assert checkRep();
1086            assert SwingUtilities.isEventDispatchThread();
1087            assert getMode() == Mode.BUILD_MODE;
1088    
1089            currentGizmo.setOrientation(
1090                (currentGizmo.getOrientation() + 90) % 360);
1091            boardView.redraw();
1092    
1093            assert checkRep();
1094        }
1095    
1096        /**
1097         * Delete currently selected Gizmo.
1098         */
1099        public void deleteCurrentGizmo() {
1100            assert checkRep();
1101            assert SwingUtilities.isEventDispatchThread();
1102            assert getMode() == Mode.BUILD_MODE;
1103    
1104            Gizmo gizmoToDelete = currentGizmo;
1105    
1106            buildModePanel.gizmosCombo.setSelectedIndex(
1107                (buildModePanel.gizmosCombo.getSelectedIndex() + 1) %
1108                buildModePanel.gizmosCombo.getItemCount());
1109    
1110            board.removeGizmo(gizmoToDelete);
1111            // FIXME: Need to de-register all trigger pairs this gizmo is a
1112            // part of...
1113    
1114            buildModePanel.refreshGizmosCombo();
1115    
1116            assert checkRep();
1117        }
1118    
1119    
1120        /*------------------------------------------------------------------
1121         * Board menu item handlers
1122         *----------------------------------------------------------------*/
1123    
1124        /**
1125         * Loads a new, empty <code>Board</code>.
1126         *
1127         * @requires <code>isClockRunning = false</code>
1128         * @effects <code>board</code> is a new, empty <code>Board</code>
1129         */
1130        public void newBoard() {
1131            assert checkRep();
1132            assert SwingUtilities.isEventDispatchThread();
1133    
1134            // TODO: Discard old Board, Clock, and SwingKeyboardInput.
1135            // Create new Board and hook it up to a new Clock and a new
1136            // SwingKeyboardInput.
1137            // make sure the user wants to do this
1138            Object[] options = {"No!",
1139            "Yes"};
1140            int makeNewBoard = JOptionPane.showOptionDialog(this,
1141                    "Are you sure you want to create\n"
1142                    + "a NEW board? You will lose all.\n"
1143                    + "unsaved changes",
1144                    "Create a new Board",
1145                    JOptionPane.YES_NO_OPTION,
1146                    JOptionPane.QUESTION_MESSAGE,
1147                    null,     //don't use a custom Icon
1148                    options,  //the titles of buttons
1149                    options[0]); //default button title
1150            switch(makeNewBoard) {
1151                    case 0: break;
1152                    case 1: {
1153                        String newBoardName = (String)JOptionPane.showInputDialog(
1154                                            this,
1155                                            "What would you like to:\n"
1156                                            + "call the new Board?",
1157                                            "Board Name",
1158                                            JOptionPane.PLAIN_MESSAGE,
1159                                            null,
1160                                            null,
1161                                            "newBoard");
1162                        
1163                        boolean loadSucceeded = true;
1164    
1165                        // FIXME: Stop clock? or specify that this must be called with a
1166                        // stopped clock?
1167    
1168                        // TODO: Do this in a separate thread...
1169    
1170                        // FIXME: Use new XMLLoadException once that's supported...
1171                        Vector<Integer> xyz = new Vector<Integer>(3);
1172                        Vector<Float> frictionVec = new Vector<Float>(2);
1173                        Vect3 grav;
1174                        float friction1, friction2;
1175                        int timeStep;
1176                        Display testDisplay;
1177                        SwingKeyboardInput testInput;
1178                        
1179                        xyz.add(0, 20);
1180                        xyz.add(1, 20);
1181                        xyz.add(2, 10);
1182                        friction1 = (float)0.025;
1183                        friction2 = (float)0.025;
1184                        frictionVec.add(0,friction1);
1185                        frictionVec.add(1,friction2);
1186                        grav = new Vect3(0,25,0);
1187                        timeStep = 40;  // 40 ms = 25 f.p.s., which is how fast the Diablo
1188                        // II engine ran at.
1189                        testDisplay = new Display();
1190                        testInput = new SwingKeyboardInput();
1191                        Board newBoard = new Board(xyz,grav,frictionVec,testDisplay,testInput);
1192                        
1193                        boardCanvas.addKeyListener(testInput.getKeyListener());
1194                        try {
1195                            BoardBuilderXML builder =
1196                                new BoardBuilderXML(new PreliminaryReleaseErrorHandler());
1197                            builder.buildFileFromBoard(newBoard, new File(newBoardName));
1198                        } catch (XMLLoadException e) {
1199                            loadSucceeded = false;
1200                            e.printStackTrace();
1201                        }
1202    
1203                        if (loadSucceeded) {
1204                            board = newBoard;
1205                            board.setDisplayMode(this);
1206                            board.setInputMode(testInput);
1207                            boardView.setBoard(board);
1208                            clock = new JavaUtilTimerClock(board, clockTickSpeed);
1209                            clock.addBoardView(boardView);
1210                            buildModePanel.refreshGizmosCombo();
1211                            boardView.redraw();
1212                        } else {
1213                            JOptionPane.showMessageDialog(this,
1214                                    "An error occurred in creating " + newBoardName + ".", "File Error",
1215                                    JOptionPane.ERROR_MESSAGE);
1216                        }
1217    
1218                        assert checkRep();
1219                    }
1220                    break;
1221                    default: break;
1222            }
1223            
1224            
1225            if (debug>0)
1226                System.out.println("newBoard() was called.");
1227    
1228            assert checkRep();
1229        }
1230    
1231        /**
1232         * Queries the user for a file and loads a <code>Board</code> from
1233         * that file.
1234         *
1235         * @requires <code>isClockRunning = false</code>
1236         * @effects <code>board</code> is the new <code>Board</code> loaded
1237         *          from the requested file if the entire procedure was
1238         *          successful, unchanged otherwise
1239         */
1240        public void openFromBoardMenu() {
1241            assert checkRep();
1242            assert SwingUtilities.isEventDispatchThread();
1243    
1244            int returnVal = fileChooser.showOpenDialog(this);
1245    
1246            if (returnVal == JFileChooser.APPROVE_OPTION) {
1247                File file = fileChooser.getSelectedFile();
1248                loadBoardFromFile(file);
1249            }
1250    
1251            assert checkRep();
1252        }
1253    
1254        /**
1255         * Saves the <code>Board</code> to the file it was loaded from. If
1256         * this <code>Board</code> was originally created by using {@link
1257         * #newBoard}, then prompt the user for a file to save it to.
1258         *
1259         * @requires <code>isClockRunning = false</code>
1260         * @effects If <code>boardFile</code> is not set, and if the user
1261         *          selects a file, then <code>boardFile</code> is set to
1262         *          the selected file.
1263         */
1264        public void saveFromBoardMenu() {
1265            assert checkRep();
1266            assert SwingUtilities.isEventDispatchThread();
1267    
1268            if (boardFile == null) {
1269                // First go to Save As to get a filename to save to
1270                saveAsFromBoardMenu();
1271            } else {
1272                if (currentGizmo != null) {
1273                    try {
1274                        board.addGizmo(currentGizmo);
1275                    } catch (NonUniqueGizmoNameException e) {
1276                        e.printStackTrace();        // Cry
1277                    }
1278                }
1279    
1280                try {
1281                    BoardBuilderXML builder =
1282                        new BoardBuilderXML(new PreliminaryReleaseErrorHandler());
1283                    builder.buildFileFromBoard(board, boardFile);
1284                } catch (XMLLoadException e) {
1285                    e.printStackTrace();
1286                    JOptionPane.showMessageDialog(this,
1287                        "An unexpected error occurred in trying to save to " +
1288                        boardFile, "Save Error", JOptionPane.ERROR_MESSAGE);
1289                }
1290    
1291                if (currentGizmo != null) {
1292                    board.removeGizmo(currentGizmo);
1293                }
1294            }
1295    
1296            assert checkRep();
1297        }
1298    
1299        /**
1300         * Queries the user for a file location and saves the
1301         * <code>Board</code> to that file.
1302         *
1303         * @requires <code>isClockRunning = false</code>
1304         * @effects If the user selects a file, then <code>boardFile</code>
1305         *          is set to the selected file.
1306         */
1307        public void saveAsFromBoardMenu() {
1308            assert checkRep();
1309            assert SwingUtilities.isEventDispatchThread();
1310    
1311            int returnVal = fileChooser.showSaveDialog(this);
1312    
1313            if (returnVal == JFileChooser.APPROVE_OPTION) {
1314                boardFile = fileChooser.getSelectedFile();
1315                saveFromBoardMenu();
1316            }
1317    
1318            assert checkRep();
1319        }
1320    
1321    
1322        /*------------------------------------------------------------------
1323         * Add Gizmo menu item handlers
1324         *----------------------------------------------------------------*/
1325    
1326        /**
1327         * Adds a <code>SquareBumper</code> to the currently loaded
1328         * <code>Board</code>.
1329         *
1330         * @requires <code>mode = Mode.BUILD_MODE</code>
1331         * @effects <code>board</code> has a <code>SquareBumper</code> added
1332         *          to it.
1333         */
1334        public void addSquareBumperToBoard() {
1335            assert checkRep();
1336            assert SwingUtilities.isEventDispatchThread();
1337            assert getMode() == Mode.BUILD_MODE;
1338    
1339            String name = JOptionPane.showInputDialog(this,
1340                "What would you like to name the new square bumper?",
1341                "Add Square Bumper", JOptionPane.QUESTION_MESSAGE);
1342    
1343            if (name == null) {
1344                // User hit cancel
1345                assert checkRep();
1346                return;
1347            }
1348    
1349            if (board.getNames().contains(name) ||
1350                    (currentGizmo != null &&
1351                     name.equals(currentGizmo.getName()))) {
1352                JOptionPane.showMessageDialog(this,
1353                    "There already is a gizmo named " + name + ".",
1354                    "Naming Error", JOptionPane.ERROR_MESSAGE);
1355            } else {
1356                Gizmo newGizmo = new SquareBumper(new Vect3(0, 0, 0));
1357                newGizmo.setName(name);
1358    
1359                boolean placeWasFound = false;
1360                zLocationLoop:
1361                for (int z = 9; z >= 0; z--) {
1362                    for (int y = 19; y >= 0; y--) {
1363                        xLocationLoop:
1364                        for (int x = 19; x >= 0; x--) {
1365                            newGizmo.moveTo(new Vect3(x, y, z));
1366    
1367                            for (Gizmo g : board) {
1368                                if (!(g instanceof OuterWalls) &&
1369                                        (GizmoBoundCalculator.doOverlap(
1370                                            newGizmo, g))) {
1371                                    continue xLocationLoop;
1372                                }
1373                            }
1374    
1375                            if (currentGizmo != null &&
1376                                    (GizmoBoundCalculator.doOverlap(
1377                                        newGizmo, currentGizmo))) {
1378                                continue xLocationLoop;
1379                            }
1380    
1381                            placeWasFound = true;
1382                            break zLocationLoop;
1383                        }
1384                    }
1385                }
1386    
1387                if (placeWasFound) {
1388                    try {
1389                        board.addGizmo(newGizmo);
1390                    } catch (NonUniqueGizmoNameException e) {
1391                        e.printStackTrace();        // Cry
1392                    }
1393                    buildModePanel.refreshGizmosCombo();
1394                    buildModePanel.gizmosCombo.setSelectedItem(newGizmo.getName());
1395                    // The setSelection will indirectly trigger a redraw...
1396                } else {
1397                    JOptionPane.showMessageDialog(this,
1398                        "A space could not be found to add the new gizmo.",
1399                        "Space Error", JOptionPane.ERROR_MESSAGE);
1400                }
1401            }
1402    
1403            assert checkRep();
1404        }
1405    
1406        /**
1407         * Adds a <code>CircularBumper</code> to the currently loaded
1408         * <code>Board</code>.
1409         *
1410         * @requires <code>mode = Mode.BUILD_MODE</code>
1411         * @effects <code>board</code> has a <code>CircularBumper</code>
1412         *          added to it.
1413         */
1414        public void addCircularBumperToBoard() {
1415            assert checkRep();
1416            assert SwingUtilities.isEventDispatchThread();
1417            assert getMode() == Mode.BUILD_MODE;
1418    
1419            String name = JOptionPane.showInputDialog(this,
1420                "What would you like to name the new circular bumper?",
1421                "Add Circular Bumper", JOptionPane.QUESTION_MESSAGE);
1422    
1423            if (name == null) {
1424                // User hit cancel
1425                assert checkRep();
1426                return;
1427            }
1428    
1429            if (board.getNames().contains(name) ||
1430                    (currentGizmo != null &&
1431                     name.equals(currentGizmo.getName()))) {
1432                JOptionPane.showMessageDialog(this,
1433                    "There already is a gizmo named " + name + ".",
1434                    "Naming Error", JOptionPane.ERROR_MESSAGE);
1435            } else {
1436                Gizmo newGizmo = new CircularBumper(new Vect3(0, 0, 0));
1437                newGizmo.setName(name);
1438    
1439                boolean placeWasFound = false;
1440                zLocationLoop:
1441                for (int z = 9; z >= 0; z--) {
1442                    for (int y = 19; y >= 0; y--) {
1443                        xLocationLoop:
1444                        for (int x = 19; x >= 0; x--) {
1445                            newGizmo.moveTo(new Vect3(x, y, z));
1446    
1447                            for (Gizmo g : board) {
1448                                if (!(g instanceof OuterWalls) &&
1449                                        (GizmoBoundCalculator.doOverlap(
1450                                            newGizmo, g))) {
1451                                    continue xLocationLoop;
1452                                }
1453                            }
1454    
1455                            if (currentGizmo != null &&
1456                                    (GizmoBoundCalculator.doOverlap(
1457                                        newGizmo, currentGizmo))) {
1458                                continue xLocationLoop;
1459                            }
1460    
1461                            placeWasFound = true;
1462                            break zLocationLoop;
1463                        }
1464                    }
1465                }
1466    
1467                if (placeWasFound) {
1468                    try {
1469                        board.addGizmo(newGizmo);
1470                    } catch (NonUniqueGizmoNameException e) {
1471                        e.printStackTrace();        // Cry
1472                    }
1473                    buildModePanel.refreshGizmosCombo();
1474                    buildModePanel.gizmosCombo.setSelectedItem(newGizmo.getName());
1475                    // The setSelection will indirectly trigger a redraw...
1476                } else {
1477                    JOptionPane.showMessageDialog(this,
1478                        "A space could not be found to add the new gizmo.",
1479                        "Space Error", JOptionPane.ERROR_MESSAGE);
1480                }
1481            }
1482    
1483            assert checkRep();
1484        }
1485    
1486        /**
1487         * Adds a <code>TriangularBumper</code> to the currently loaded
1488         * <code>Board</code>.
1489         *
1490         * @requires <code>mode = Mode.BUILD_MODE</code>
1491         * @effects <code>board</code> has a <code>TriangularBumper</code>
1492         *          added to it.
1493         */
1494        public void addTriangularBumperToBoard() {
1495            assert checkRep();
1496            assert SwingUtilities.isEventDispatchThread();
1497            assert getMode() == Mode.BUILD_MODE;
1498    
1499            String name = JOptionPane.showInputDialog(this,
1500                "What would you like to name the new triangular bumper?",
1501                "Add Triangular Bumper", JOptionPane.QUESTION_MESSAGE);
1502    
1503            if (name == null) {
1504                // User hit cancel
1505                assert checkRep();
1506                return;
1507            }
1508    
1509            if (board.getNames().contains(name) ||
1510                    (currentGizmo != null &&
1511                     name.equals(currentGizmo.getName()))) {
1512                JOptionPane.showMessageDialog(this,
1513                    "There already is a gizmo named " + name + ".",
1514                    "Naming Error", JOptionPane.ERROR_MESSAGE);
1515            } else {
1516                Gizmo newGizmo = new TriangularBumper(new Vect3(0, 0, 0), 0);
1517                newGizmo.setName(name);
1518    
1519                boolean placeWasFound = false;
1520                zLocationLoop:
1521                for (int z = 9; z >= 0; z--) {
1522                    for (int y = 19; y >= 0; y--) {
1523                        xLocationLoop:
1524                        for (int x = 19; x >= 0; x--) {
1525                            newGizmo.moveTo(new Vect3(x, y, z));
1526    
1527                            for (Gizmo g : board) {
1528                                if (!(g instanceof OuterWalls) &&
1529                                        (GizmoBoundCalculator.doOverlap(
1530                                            newGizmo, g))) {
1531                                    continue xLocationLoop;
1532                                }
1533                            }
1534    
1535                            if (currentGizmo != null &&
1536                                    (GizmoBoundCalculator.doOverlap(
1537                                        newGizmo, currentGizmo))) {
1538                                continue xLocationLoop;
1539                            }
1540    
1541                            placeWasFound = true;
1542                            break zLocationLoop;
1543                        }
1544                    }
1545                }
1546    
1547                if (placeWasFound) {
1548                    try {
1549                        board.addGizmo(newGizmo);
1550                    } catch (NonUniqueGizmoNameException e) {
1551                        e.printStackTrace();        // Cry
1552                    }
1553                    buildModePanel.refreshGizmosCombo();
1554                    buildModePanel.gizmosCombo.setSelectedItem(newGizmo.getName());
1555                    // The setSelection will indirectly trigger a redraw...
1556                } else {
1557                    JOptionPane.showMessageDialog(this,
1558                        "A space could not be found to add the new gizmo.",
1559                        "Space Error", JOptionPane.ERROR_MESSAGE);
1560                }
1561            }
1562    
1563            assert checkRep();
1564        }
1565    
1566        /**
1567         * Adds a <code>LeftFlipper</code> to the currently loaded
1568         * <code>Board</code>.
1569         *
1570         * @requires <code>mode = Mode.BUILD_MODE</code>
1571         * @effects <code>board</code> has a <code>LeftFlipper</code> added
1572         *          to it.
1573         */
1574        public void addLeftFlipperToBoard() {
1575            assert checkRep();
1576            assert SwingUtilities.isEventDispatchThread();
1577            assert getMode() == Mode.BUILD_MODE;
1578    
1579            String name = JOptionPane.showInputDialog(this,
1580                "What would you like to name the new left flipper?",
1581                "Add Left Flipper", JOptionPane.QUESTION_MESSAGE);
1582    
1583            if (name == null) {
1584                // User hit cancel
1585                assert checkRep();
1586                return;
1587            }
1588    
1589            if (board.getNames().contains(name) ||
1590                    (currentGizmo != null &&
1591                     name.equals(currentGizmo.getName()))) {
1592                JOptionPane.showMessageDialog(this,
1593                    "There already is a gizmo named " + name + ".",
1594                    "Naming Error", JOptionPane.ERROR_MESSAGE);
1595            } else {
1596                Gizmo newGizmo = new LeftFlipper(new Vect3(0, 0, 0), 0);
1597                newGizmo.setName(name);
1598    
1599                boolean placeWasFound = false;
1600                zLocationLoop:
1601                for (int z = 9; z >= 0; z--) {
1602                    for (int y = 18; y >= 0; y--) {
1603                        xLocationLoop:
1604                        for (int x = 18; x >= 0; x--) {
1605                            newGizmo.moveTo(new Vect3(x, y, z));
1606    
1607                            for (Gizmo g : board) {
1608                                if (!(g instanceof OuterWalls) &&
1609                                        (GizmoBoundCalculator.doOverlap(
1610                                            newGizmo, g))) {
1611                                    continue xLocationLoop;
1612                                }
1613                            }
1614    
1615                            if (currentGizmo != null &&
1616                                    (GizmoBoundCalculator.doOverlap(
1617                                        newGizmo, currentGizmo))) {
1618                                continue xLocationLoop;
1619                            }
1620    
1621                            placeWasFound = true;
1622                            break zLocationLoop;
1623                        }
1624                    }
1625                }
1626    
1627                if (placeWasFound) {
1628                    try {
1629                        board.addGizmo(newGizmo);
1630                    } catch (NonUniqueGizmoNameException e) {
1631                        e.printStackTrace();        // Cry
1632                    }
1633                    buildModePanel.refreshGizmosCombo();
1634                    buildModePanel.gizmosCombo.setSelectedItem(newGizmo.getName());
1635                    // The setSelection will indirectly trigger a redraw...
1636                } else {
1637                    JOptionPane.showMessageDialog(this,
1638                        "A space could not be found to add the new gizmo.",
1639                        "Space Error", JOptionPane.ERROR_MESSAGE);
1640                }
1641            }
1642    
1643            assert checkRep();
1644        }
1645    
1646        /**
1647         * Adds a <code>RightFlipper</code> to the currently loaded
1648         * <code>Board</code>.
1649         *
1650         * @requires <code>mode = Mode.BUILD_MODE</code>
1651         * @effects <code>board</code> has a <code>RightFlipper</code> added
1652         *          to it.
1653         */
1654        public void addRightFlipperToBoard() {
1655            assert checkRep();
1656            assert SwingUtilities.isEventDispatchThread();
1657            assert getMode() == Mode.BUILD_MODE;
1658    
1659            String name = JOptionPane.showInputDialog(this,
1660                "What would you like to name the new right flipper?",
1661                "Add Right Flipper", JOptionPane.QUESTION_MESSAGE);
1662    
1663            if (name == null) {
1664                // User hit cancel
1665                assert checkRep();
1666                return;
1667            }
1668    
1669            if (board.getNames().contains(name) ||
1670                    (currentGizmo != null &&
1671                     name.equals(currentGizmo.getName()))) {
1672                JOptionPane.showMessageDialog(this,
1673                    "There already is a gizmo named " + name + ".",
1674                    "Naming Error", JOptionPane.ERROR_MESSAGE);
1675            } else {
1676                Gizmo newGizmo = new RightFlipper(new Vect3(0, 0, 0), 0);
1677                newGizmo.setName(name);
1678    
1679                boolean placeWasFound = false;
1680                zLocationLoop:
1681                for (int z = 9; z >= 0; z--) {
1682                    for (int y = 18; y >= 0; y--) {
1683                        xLocationLoop:
1684                        for (int x = 18; x >= 0; x--) {
1685                            newGizmo.moveTo(new Vect3(x, y, z));
1686    
1687                            for (Gizmo g : board) {
1688                                if (!(g instanceof OuterWalls) &&
1689                                        (GizmoBoundCalculator.doOverlap(
1690                                            newGizmo, g))) {
1691                                    continue xLocationLoop;
1692                                }
1693                            }
1694    
1695                            if (currentGizmo != null &&
1696                                    (GizmoBoundCalculator.doOverlap(
1697                                        newGizmo, currentGizmo))) {
1698                                continue xLocationLoop;
1699                            }
1700    
1701                            placeWasFound = true;
1702                            break zLocationLoop;
1703                        }
1704                    }
1705                }
1706    
1707                if (placeWasFound) {
1708                    try {
1709                        board.addGizmo(newGizmo);
1710                    } catch (NonUniqueGizmoNameException e) {
1711                        e.printStackTrace();        // Cry
1712                    }
1713                    buildModePanel.refreshGizmosCombo();
1714                    buildModePanel.gizmosCombo.setSelectedItem(newGizmo.getName());
1715                    // The setSelection will indirectly trigger a redraw...
1716                } else {
1717                    JOptionPane.showMessageDialog(this,
1718                        "A space could not be found to add the new gizmo.",
1719                        "Space Error", JOptionPane.ERROR_MESSAGE);
1720                }
1721            }
1722    
1723            assert checkRep();
1724        }
1725    
1726        /**
1727         * Adds an <code>Absorber</code> to the currently loaded
1728         * <code>Board</code>.
1729         *
1730         * @requires <code>mode = Mode.BUILD_MODE</code>
1731         * @effects <code>board</code> has an <code>Absorber</code> added to
1732         *          it.
1733         */
1734        public void addAbsorberToBoard() {
1735            assert checkRep();
1736            assert SwingUtilities.isEventDispatchThread();
1737            assert getMode() == Mode.BUILD_MODE;
1738    
1739            // XXX: Four dialogs to create an absorber is really annoying...
1740    
1741            String name = JOptionPane.showInputDialog(this,
1742                "What would you like to name the new absorber?",
1743                "Add Absorber (Step 1 of 4)", JOptionPane.QUESTION_MESSAGE);
1744            if (name == null) {
1745                // User hit cancel
1746                assert checkRep();
1747                return;
1748            } else if (board.getNames().contains(name) ||
1749                       (currentGizmo != null &&
1750                        name.equals(currentGizmo.getName()))) {
1751                JOptionPane.showMessageDialog(this,
1752                    "There already is a gizmo named " + name + ".",
1753                    "Naming Error", JOptionPane.ERROR_MESSAGE);
1754                assert checkRep();
1755                return;
1756            }
1757    
1758            String widthString = JOptionPane.showInputDialog(this,
1759                "What would you like the width of the new absorber to be?",
1760                "Add Absorber (Step 2 of 4)", JOptionPane.QUESTION_MESSAGE);
1761            if (widthString == null) {
1762                // User hit cancle
1763                assert checkRep();
1764                return;
1765            }
1766            int width;
1767            try {
1768                width = Integer.parseInt(widthString);
1769            } catch (NumberFormatException e) {
1770                JOptionPane.showMessageDialog(this,
1771                    widthString + " is not an integer.", "Format Error",
1772                    JOptionPane.ERROR_MESSAGE);
1773                assert checkRep();
1774                return;
1775            }
1776            if (width <= 0) {
1777                JOptionPane.showMessageDialog(this,
1778                    "The width must be a positive integer.", "Sign Error",
1779                    JOptionPane.ERROR_MESSAGE);
1780            }
1781    
1782            String heightString = JOptionPane.showInputDialog(this,
1783                "What would you like the height of the new absorber to be?",
1784                "Add Absorber (Step 3 of 4)", JOptionPane.QUESTION_MESSAGE);
1785            if (heightString == null) {
1786                // User hit cancle
1787                assert checkRep();
1788                return;
1789            }
1790            int height;
1791            try {
1792                height = Integer.parseInt(heightString);
1793            } catch (NumberFormatException e) {
1794                JOptionPane.showMessageDialog(this,
1795                    heightString + " is not an integer.", "Format Error",
1796                    JOptionPane.ERROR_MESSAGE);
1797                assert checkRep();
1798                return;
1799            }
1800            if (height <= 0) {
1801                JOptionPane.showMessageDialog(this,
1802                    "The height must be a positive integer.", "Sign Error",
1803                    JOptionPane.ERROR_MESSAGE);
1804            }
1805    
1806            String depthString = JOptionPane.showInputDialog(this,
1807                "What would you like the depth of the new absorber to be?",
1808                "Add Absorber (Step 4 of 4)", JOptionPane.QUESTION_MESSAGE);
1809            if (depthString == null) {
1810                // User hit cancle
1811                assert checkRep();
1812                return;
1813            }
1814            int depth;
1815            try {
1816                depth = Integer.parseInt(depthString);
1817            } catch (NumberFormatException e) {
1818                JOptionPane.showMessageDialog(this,
1819                    depthString + " is not an integer.", "Format Error",
1820                    JOptionPane.ERROR_MESSAGE);
1821                assert checkRep();
1822                return;
1823            }
1824            if (depth <= 0) {
1825                JOptionPane.showMessageDialog(this,
1826                    "The depth must be a positive integer.", "Sign Error",
1827                    JOptionPane.ERROR_MESSAGE);
1828            }
1829    
1830    
1831            Gizmo newGizmo = new Absorber(new Vect3(0, 0, 0),
1832                                          width, height, depth);
1833            newGizmo.setName(name);
1834    
1835            boolean placeWasFound = false;
1836            zLocationLoop:
1837            for (int z = 10 - depth; z >= 0; z--) {
1838                for (int y = 20 - height; y >= 0; y--) {
1839                    xLocationLoop:
1840                    for (int x = 20 - width; x >= 0; x--) {
1841                        newGizmo.moveTo(new Vect3(x, y, z));
1842    
1843                        for (Gizmo g : board) {
1844                            if (!(g instanceof OuterWalls) &&
1845                                    (GizmoBoundCalculator.doOverlap(
1846                                        newGizmo, g))) {
1847                                continue xLocationLoop;
1848                            }
1849                        }
1850    
1851                        if (currentGizmo != null &&
1852                                (GizmoBoundCalculator.doOverlap(
1853                                    newGizmo, currentGizmo))) {
1854                            continue xLocationLoop;
1855                        }
1856    
1857                        placeWasFound = true;
1858                        break zLocationLoop;
1859                    }
1860                }
1861            }
1862    
1863            if (placeWasFound) {
1864                try {
1865                    board.addGizmo(newGizmo);
1866                } catch (NonUniqueGizmoNameException e) {
1867                    e.printStackTrace();        // Cry
1868                }
1869                buildModePanel.refreshGizmosCombo();
1870                buildModePanel.gizmosCombo.setSelectedItem(newGizmo.getName());
1871                // The setSelection will indirectly trigger a redraw...
1872            } else {
1873                JOptionPane.showMessageDialog(this,
1874                    "A space could not be found to add the new gizmo.",
1875                    "Space Error", JOptionPane.ERROR_MESSAGE);
1876            }
1877    
1878            assert checkRep();
1879        }
1880    
1881        /**
1882         * Adds an <code>Ball</code> to the currently loaded
1883         * <code>Board</code>.
1884         *
1885         * @requires <code>mode = Mode.BUILD_MODE</code>
1886         * @effects <code>board</code> has an <code>Ball</code> added to it.
1887         */
1888        public void addBallToBoard() {
1889            assert checkRep();
1890            assert SwingUtilities.isEventDispatchThread();
1891            assert getMode() == Mode.BUILD_MODE;
1892            
1893            if (debug > 0)
1894                System.out.println("addBallToBoard() was called.");
1895            
1896            assert getMode() == Mode.BUILD_MODE;
1897    
1898            String name = JOptionPane.showInputDialog(this,
1899                "What would you like to name the new ball?",
1900                "Add Ball", JOptionPane.QUESTION_MESSAGE);
1901    
1902            if (name == null) {
1903                // User hit cancel
1904                assert checkRep();
1905                return;
1906            }
1907    
1908            if (board.getNames().contains(name) ||
1909                    (currentGizmo != null &&
1910                     name.equals(currentGizmo.getName()))) {
1911                JOptionPane.showMessageDialog(this,
1912                    "There already is a gizmo named " + name + ".",
1913                    "Naming Error", JOptionPane.ERROR_MESSAGE);
1914            } else {
1915                //String xPos = JOptionPane.showInputDialog(this,
1916                //        "Starting X coordinate?",
1917                //        "Add Ball", JOptionPane.QUESTION_MESSAGE);
1918                
1919                
1920                Gizmo newGizmo = new Ball(new Vect3(0, 0, 0), new Vect3(0,0,0));
1921                newGizmo.setName(name);
1922    
1923                boolean placeWasFound = false;
1924                zLocationLoop:
1925                for (int z = 9; z >= 0; z--) {
1926                    for (int y = 18; y >= 0; y--) {
1927                        xLocationLoop:
1928                        for (int x = 18; x >= 0; x--) {
1929                            newGizmo.moveTo(new Vect3(x, y, z));
1930    
1931                            for (Gizmo g : board) {
1932                                if (!(g instanceof OuterWalls) &&
1933                                        (GizmoBoundCalculator.doOverlap(
1934                                            newGizmo, g))) {
1935                                    continue xLocationLoop;
1936                                }
1937                            }
1938    
1939                            if (currentGizmo != null &&
1940                                    (GizmoBoundCalculator.doOverlap(
1941                                        newGizmo, currentGizmo))) {
1942                                continue xLocationLoop;
1943                            }
1944    
1945                            placeWasFound = true;
1946                            break zLocationLoop;
1947                        }
1948                    }
1949                }
1950    
1951                if (placeWasFound) {
1952                    try {
1953                        board.addGizmo(newGizmo);
1954                    } catch (NonUniqueGizmoNameException e) {
1955                        e.printStackTrace();        // Cry
1956                    }
1957                    buildModePanel.refreshGizmosCombo();
1958                    buildModePanel.gizmosCombo.setSelectedItem(newGizmo.getName());
1959                    // The setSelection will indirectly trigger a redraw...
1960                } else {
1961                    JOptionPane.showMessageDialog(this,
1962                        "A space could not be found to add the new gizmo.",
1963                        "Space Error", JOptionPane.ERROR_MESSAGE);
1964                }
1965            }
1966            
1967            
1968            assert checkRep();
1969        }
1970    
1971    
1972        /*------------------------------------------------------------------
1973         * Connections menu item handlers
1974         *----------------------------------------------------------------*/
1975    
1976        /**
1977         * Provides the user an interface to modify gizmo-gizmo connections.
1978         */
1979        public void gizmoConnectModify() {
1980            
1981            assert checkRep();
1982            assert SwingUtilities.isEventDispatchThread();
1983            
1984            String sourceGizmo = JOptionPane.showInputDialog(this,
1985                    "What is the name of the source gizmo?"
1986                    , JOptionPane.QUESTION_MESSAGE);
1987            String targetGizmo = JOptionPane.showInputDialog(this,
1988                    "What is the name of the target gizmo?"
1989                    , JOptionPane.QUESTION_MESSAGE);
1990            
1991            
1992            if((board.getNames().contains(sourceGizmo) || currentGizmo == null ||
1993                    currentGizmo.getName().equals(sourceGizmo))
1994                    && (board.getNames().contains(targetGizmo) || currentGizmo == 
1995                        null || currentGizmo.getName().equals(targetGizmo))){
1996                
1997                Gizmo sourceGizmoObject;
1998                Gizmo targetGizmoObject;
1999                
2000                if(!sourceGizmo.equals(currentGizmo.getName())){
2001                        sourceGizmoObject = board.checkoutGizmo(sourceGizmo);
2002                }
2003                else{
2004                    sourceGizmoObject = currentGizmo;
2005                }
2006                if(!targetGizmo.equals(currentGizmo.getName())){
2007                    targetGizmoObject = board.checkoutGizmo(targetGizmo);
2008                }
2009                else{
2010                    targetGizmoObject = currentGizmo;
2011                }
2012                try{
2013                    targetGizmoObject.connectToTriggerGenerator(sourceGizmoObject);
2014                    if(!sourceGizmoObject.equals(targetGizmoObject)){
2015                        board.addGizmo(sourceGizmoObject);
2016                    }
2017                    board.addGizmo(targetGizmoObject);
2018                }
2019                catch(Exception e)
2020                {
2021                    JOptionPane.showMessageDialog(this, "Connect failed");
2022                }
2023            }
2024            else{
2025                JOptionPane.showMessageDialog(this, "Invalid Gizmo name(s), not found "
2026                    + "in board");
2027            }
2028            
2029            assert checkRep();
2030        }
2031    
2032        /**
2033         * Provides the user an interface to modify key connections.
2034         */
2035        public void keyConnectModify() {
2036            assert checkRep();
2037            assert SwingUtilities.isEventDispatchThread();
2038    
2039            String asciiInput = JOptionPane.showInputDialog(this,
2040                "What is the ASCII key code of the key you want to modify?",
2041                "Edit Keyboard Connections", JOptionPane.QUESTION_MESSAGE);
2042    
2043            if (asciiInput == null) {
2044                // User pressed Cancel
2045                assert checkRep();
2046                return;
2047            }
2048    
2049            int asciiCode;
2050            try {
2051                asciiCode = Integer.parseInt(asciiInput);
2052            } catch (NumberFormatException e) {
2053                JOptionPane.showMessageDialog(this,
2054                    "The ASCII key code must be a number between 0 and 127.",
2055                    "Format Error", JOptionPane.ERROR_MESSAGE);
2056                assert checkRep();
2057                return;
2058            }
2059    
2060            if (asciiCode < 0 || asciiCode > 127) {
2061                JOptionPane.showMessageDialog(this,
2062                    "The ASCII key code must be a number between 0 and 127.",
2063                    "Format Error", JOptionPane.ERROR_MESSAGE);
2064                assert checkRep();
2065                return;
2066            }
2067    
2068            Object[] options = { "Key Pressed", "Key Released" };
2069            int n = JOptionPane.showOptionDialog(this,
2070                    "Which type of key events are you interested in?",
2071                    "Edit Keyboard Connections", JOptionPane.YES_NO_OPTION,
2072                    JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
2073    
2074            KeyboardTriggerGenerator ktg;
2075            if (n == 0) {
2076                ktg = keyboardInput.getKeyboardTriggerGenerator(asciiCode,
2077                        KeyboardInput.EventType.KEY_UP);
2078            } else {
2079                ktg = keyboardInput.getKeyboardTriggerGenerator(asciiCode,
2080                        KeyboardInput.EventType.KEY_DOWN);
2081            }
2082    
2083            String gizmoName = JOptionPane.showInputDialog(this,
2084            "What is the name of the gizmo you'd like to be trigger by this key?",
2085            "Edit Keyboard Connections", JOptionPane.QUESTION_MESSAGE);
2086    
2087            if (gizmoName == null) {
2088                assert checkRep();
2089                return;
2090            }
2091    
2092            if (currentGizmo != null && gizmoName.equals(currentGizmo.getName())) {
2093                currentGizmo.connectToTriggerGenerator(ktg);
2094            } else if (board.getNames().contains(gizmoName)) {
2095                Gizmo gizmo = board.checkoutGizmo(gizmoName);
2096                gizmo.connectToTriggerGenerator(ktg);
2097                try {
2098                    board.addGizmo(gizmo);
2099                } catch (NonUniqueGizmoNameException e) {
2100                    e.printStackTrace(); // Cry
2101                }
2102            } else {
2103                JOptionPane.showMessageDialog(this,
2104                    "The gizmo " + gizmoName + " was not found.", "Name Error",
2105                    JOptionPane.ERROR_MESSAGE);
2106            }
2107    
2108            assert checkRep();
2109        }
2110    
2111    
2112        /*------------------------------------------------------------------
2113         * Help menu item handlers
2114         *----------------------------------------------------------------*/
2115    
2116        /**
2117         * Launches a web browser that shows the Gizmoball user guide.
2118         */
2119        public void userGuideFromHelpMenu() {
2120            assert checkRep();
2121            assert SwingUtilities.isEventDispatchThread();
2122    
2123            BareBonesBrowserLaunch.openURL("http://web.mit.edu/gleitz/Public/gizmoball/manual/");
2124            if (debug > 0)
2125                System.out.println("userGuideFromHelpMenu() was called.");
2126    
2127            assert checkRep();
2128        }
2129        
2130    
2131        /**
2132         * Shows the About dialog.
2133         */
2134        public void aboutFromHelpMenu() {
2135            assert checkRep();
2136            assert SwingUtilities.isEventDispatchThread();
2137    
2138            // display a dialog with icon
2139            JOptionPane.showMessageDialog(null,
2140                    "Gizmoball\n" +
2141                    "Copyright Gadgix 2007\n" +
2142                    "Version 2.1.1\n" +
2143                     "$Id: Display.java 691 2007-05-14 23:50:08Z billmag $\n" +
2144                    "\n" +
2145                    "Created by billmag, eefi, gleitz, and jhawk\n");
2146            if (debug > 0)
2147                System.out.println("aboutFromHelpMenu() was called.");
2148    
2149            assert checkRep();
2150        }
2151    
2152    
2153        /*------------------------------------------------------------------
2154         * public enum Mode
2155         *----------------------------------------------------------------*/
2156    
2157        /**
2158         * The different modes the Gizmoball application supports.
2159         */
2160        public enum Mode {
2161            
2162            /**
2163             * A mode in which the user may modify the current
2164             * <code>Board</code>.
2165             */
2166            BUILD_MODE,
2167    
2168            /**
2169             * A mode in which the application simulates the
2170             * <code>Board</code>.
2171             */
2172            RUN_MODE;
2173        }
2174    
2175    
2176        /*------------------------------------------------------------------
2177         * private class RunModePanel
2178         *----------------------------------------------------------------*/
2179    
2180        // The right bar in running mode.
2181        private class RunModePanel extends JPanel {
2182    
2183            // Starts the clock.
2184            private final JButton playButton;
2185    
2186            // Stops the clock.
2187            private final JButton pauseButton;
2188    
2189            // Resets the board. (Moves the balls back to their initial
2190            // positions and velocities.)
2191            // private final JButton resetButton;
2192    
2193            // XXX: To quench a compiler warning; should be set to better
2194            // value later.
2195            private static final long serialVersionUID = 1L;
2196    
2197            // Constructs a RunModePanel with all the controls constructed
2198            // and actions hooked up. This RunModePanel will be set up for
2199            // paused mode.
2200            public RunModePanel() {
2201                SpringLayout layout = new SpringLayout();
2202                setLayout(layout);
2203                SpringLayout.Constraints panelCons = layout.getConstraints(this);
2204    
2205                // Control construction ------------------------------------
2206                playButton = new JButton("Play");
2207                add(playButton);
2208                SpringLayout.Constraints playButtonCons =
2209                    layout.getConstraints(playButton);
2210    
2211                pauseButton = new JButton("Pause");
2212                pauseButton.setEnabled(false);
2213                add(pauseButton);
2214                SpringLayout.Constraints pauseButtonCons =
2215                    layout.getConstraints(pauseButton);
2216    
2217    // resetButton = new JButton("Reset");
2218    // add(resetButton);
2219    // SpringLayout.Constraints resetButtonCons =
2220    //     layout.getConstraints(resetButton);
2221    
2222    
2223                // Control layout ------------------------------------------
2224    
2225                Spring padding = Spring.constant(12);
2226    
2227                // Line up left edges of all the buttons along the left edge
2228                // of the panel
2229                playButtonCons.setX(Spring.sum(panelCons.getX(), padding));
2230                pauseButtonCons.setX(playButtonCons.getX());
2231                // resetButtonCons.setX(playButtonCons.getX());
2232    
2233                // Set widths of all buttons to width of widest one.
2234                // XXX: Pause is currently hardcoded as the widest.  Should
2235                // be more flexible...
2236                playButtonCons.setWidth(pauseButtonCons.getWidth());
2237                // resetButtonCons.setWidth(pauseButtonCons.getWidth());
2238    
2239                // Put Play, Pause, and Reset at top and space them out
2240                playButtonCons.setY(Spring.sum(panelCons.getY(), padding));
2241                pauseButtonCons.setY(
2242                    Spring.sum(playButtonCons.getConstraint(SpringLayout.SOUTH),
2243                               padding));
2244                // resetButtonCons.setY(
2245                //     Spring.sum(pauseButtonCons.getConstraint(SpringLayout.SOUTH),
2246                //                padding));
2247    
2248                // Event handling ------------------------------------------
2249    
2250                playButton.addActionListener(new ActionListener() {
2251                    public void actionPerformed(ActionEvent e) {
2252                        startTime();
2253                    }
2254                });
2255    
2256                pauseButton.addActionListener(new ActionListener() {
2257                    public void actionPerformed(ActionEvent e) {
2258                        stopTime();
2259                    }
2260                });
2261    
2262                // resetButton.addActionListener(new ActionListener() {
2263                //     public void actionPerformed(ActionEvent e) {
2264                //         resetBoard();
2265                //     }
2266                // });
2267    
2268            }
2269    
2270            // Enables or disables buttons depending on whether the clock is
2271            // running. Expects true if the clock is running and false if
2272            // the clock is not running.
2273            public void setClockRunning(boolean isClockRunning) {
2274                if (isClockRunning) {
2275                    playButton.setEnabled(false);
2276                    pauseButton.setEnabled(true);
2277                    // resetButton.setEnabled(false);
2278                } else {
2279                    playButton.setEnabled(true);
2280                    pauseButton.setEnabled(false);
2281                    // resetButton.setEnabled(true);
2282                }
2283            }
2284        }
2285    
2286    
2287        /*------------------------------------------------------------------
2288         * private class BuildModePanel
2289         *----------------------------------------------------------------*/
2290    
2291        // The right bar in building mode.
2292        private class BuildModePanel extends JPanel {
2293    
2294            // Combo box containing list of Gizmo names
2295            private final JComboBox gizmosCombo;
2296    
2297            // XXX: To quench a compiler warning; should be set to better
2298            // value later.
2299            private static final long serialVersionUID = 1L;
2300    
2301            // Constructs a BuildModePanel with all the controls constructed
2302            // and actions hooked up.
2303            private BuildModePanel() {
2304                SpringLayout layout = new SpringLayout();
2305                setLayout(layout);
2306                SpringLayout.Constraints panelCons =
2307                    layout.getConstraints(this);
2308    
2309                // Control construction ------------------------------------
2310    
2311                JLabel gizmosComboLabel = new JLabel("Current gizmo:");
2312                add(gizmosComboLabel);
2313                SpringLayout.Constraints gizmosComboLabelCons =
2314                    layout.getConstraints(gizmosComboLabel);
2315    
2316                gizmosCombo = new JComboBox();
2317                add(gizmosCombo);
2318                SpringLayout.Constraints gizmosComboCons =
2319                    layout.getConstraints(gizmosCombo);
2320    
2321                // JButton rename = new JButton("Rename Gizmo");
2322                // add(rename);
2323                // SpringLayout.Constraints renameCons =
2324                //     layout.getConstraints(rename);
2325    
2326                JLabel moveLabel = new JLabel("Move Gizmo");
2327                add(moveLabel);
2328                SpringLayout.Constraints moveLabelCons =
2329                    layout.getConstraints(moveLabel);
2330    
2331                JButton moveUp = new JButton("Up");
2332                add(moveUp);
2333                SpringLayout.Constraints moveUpCons =
2334                    layout.getConstraints(moveUp);
2335    
2336                JButton moveLeft = new JButton("Left");
2337                add(moveLeft);
2338                SpringLayout.Constraints moveLeftCons =
2339                    layout.getConstraints(moveLeft);
2340    
2341                JButton moveRight = new JButton("Right");
2342                add(moveRight);
2343                SpringLayout.Constraints moveRightCons =
2344                    layout.getConstraints(moveRight);
2345    
2346                JButton moveDown = new JButton("Down");
2347                add(moveDown);
2348                SpringLayout.Constraints moveDownCons =
2349                    layout.getConstraints(moveDown);
2350    
2351                JButton moveBackward = new JButton("Backward");
2352                add(moveBackward);
2353                SpringLayout.Constraints moveBackwardCons =
2354                    layout.getConstraints(moveBackward);
2355    
2356                JButton moveForward = new JButton("Forward");
2357                add(moveForward);
2358                SpringLayout.Constraints moveForwardCons =
2359                    layout.getConstraints(moveForward);
2360    
2361                JLabel rotateLabel = new JLabel("Rotate Gizmo");
2362                add(rotateLabel);
2363                SpringLayout.Constraints rotateLabelCons =
2364                    layout.getConstraints(rotateLabel);
2365    
2366                JButton rotateClockwise = new JButton("Clockwise");
2367                add(rotateClockwise);
2368                SpringLayout.Constraints rotateClockwiseCons =
2369                    layout.getConstraints(rotateClockwise);
2370    
2371                JButton rotateCounterclockwise = new JButton("Counterclockwise");
2372                add(rotateCounterclockwise);
2373                SpringLayout.Constraints rotateCounterclockwiseCons =
2374                    layout.getConstraints(rotateCounterclockwise);
2375    
2376                JButton deleteGizmo = new JButton("Delete Gizmo");
2377                add(deleteGizmo);
2378                SpringLayout.Constraints deleteGizmoCons =
2379                    layout.getConstraints(deleteGizmo);
2380    
2381    
2382                // Control layout ------------------------------------------
2383    
2384                Spring padding = Spring.constant(12);
2385                Spring groupPadding = Spring.constant(36);
2386                Spring labelPadding = Spring.constant(6);
2387                Spring indent = Spring.constant(18);
2388    
2389                Spring leftEdge = Spring.sum(panelCons.getX(), padding);
2390                Spring leftIndentedEdge = Spring.sum(leftEdge, indent);
2391    
2392                gizmosComboLabelCons.setX(leftEdge);
2393                gizmosComboLabelCons.setY(Spring.sum(panelCons.getY(), padding));
2394    
2395                gizmosComboCons.setY(Spring.sum(
2396                    gizmosComboLabelCons.getConstraint(SpringLayout.SOUTH),
2397                    labelPadding));
2398                // Setting x edges needs to wait until we have the width
2399                // from the move diamond.
2400    
2401                // renameCons.setY(Spring.sum(
2402                //     gizmosComboCons.getConstraint(SpringLayout.SOUTH), padding));
2403                // Setting x location needs to wait until we have the width
2404                // from the move diamond.
2405    
2406                moveLabelCons.setX(leftEdge);
2407                moveLabelCons.setY(Spring.sum(
2408                    gizmosComboCons.getConstraint(SpringLayout.SOUTH),
2409                    groupPadding));
2410    
2411                moveUpCons.setY(Spring.sum(
2412                    moveLabelCons.getConstraint(SpringLayout.SOUTH), padding));
2413                moveLeftCons.setY(Spring.sum(
2414                    moveUpCons.getConstraint(SpringLayout.SOUTH), padding));
2415                moveRightCons.setY(moveLeftCons.getY());
2416                moveDownCons.setY(Spring.sum(
2417                    moveLeftCons.getConstraint(SpringLayout.SOUTH), padding));
2418    
2419                // XXX: Assumes "Right" is wider than "Left" and "Down" is
2420                // wider than "Up".
2421                moveLeftCons.setWidth(moveRightCons.getWidth());
2422                moveUpCons.setWidth(moveDownCons.getWidth());
2423    
2424                moveLeftCons.setX(leftIndentedEdge);
2425                moveUpCons.setX(Spring.sum(
2426                    moveLeftCons.getConstraint(SpringLayout.EAST), padding));
2427                moveDownCons.setX(moveUpCons.getX());
2428                moveRightCons.setX(Spring.sum(
2429                    moveUpCons.getConstraint(SpringLayout.EAST), padding));
2430    
2431                Spring rightEdge = moveRightCons.getConstraint(SpringLayout.EAST);
2432                panelCons.setConstraint(SpringLayout.EAST,
2433                    Spring.sum(rightEdge, padding));
2434                gizmosComboCons.setConstraint(SpringLayout.EAST, rightEdge);
2435                gizmosComboCons.setX(leftEdge);
2436                //renameCons.setConstraint(SpringLayout.EAST, rightEdge);
2437    
2438                // XXX: Assumes "Backward" is wider than "Forward" and that
2439                // the two side by side are less wide than the diamond
2440                // above.
2441                moveForwardCons.setWidth(moveBackwardCons.getWidth());
2442                moveBackwardCons.setX(leftIndentedEdge);
2443                moveBackwardCons.setY(Spring.sum(
2444                    moveDownCons.getConstraint(SpringLayout.SOUTH), padding));
2445                moveForwardCons.setConstraint(SpringLayout.EAST, rightEdge);
2446                moveForwardCons.setY(moveBackwardCons.getY());
2447    
2448                rotateLabelCons.setX(leftEdge);
2449                rotateLabelCons.setY(Spring.sum(
2450                    moveBackwardCons.getConstraint(SpringLayout.SOUTH),
2451                    groupPadding));
2452    
2453                rotateClockwiseCons.setX(leftIndentedEdge);
2454                rotateClockwiseCons.setY(Spring.sum(
2455                    rotateLabelCons.getConstraint(SpringLayout.SOUTH), padding));
2456                rotateCounterclockwiseCons.setX(leftIndentedEdge);
2457                rotateCounterclockwiseCons.setY(Spring.sum(
2458                    rotateClockwiseCons.getConstraint(SpringLayout.SOUTH),
2459                    padding));
2460    
2461                // XXX: Assumes "Counterclockwise" is wider than
2462                // "Clockwise".
2463                rotateClockwiseCons.setWidth(
2464                    rotateCounterclockwiseCons.getWidth());
2465    
2466                deleteGizmoCons.setX(leftEdge);
2467                deleteGizmoCons.setY(Spring.sum(
2468                    rotateCounterclockwiseCons.getConstraint(SpringLayout.SOUTH),
2469                    groupPadding));
2470    
2471    
2472                // Event handling ------------------------------------------
2473    
2474                gizmosCombo.addItemListener(new ItemListener() {
2475                    public void itemStateChanged(ItemEvent e) {
2476                        if (rightBar.getSelectedIndex() == 0) {
2477                            // We're in run mode, so ignore everything
2478                            return;
2479                        }
2480    
2481                        if (currentGizmo != null) {
2482                            try {
2483                                board.addGizmo(currentGizmo);
2484                            } catch (NonUniqueGizmoNameException ex) {
2485                                // Cry
2486                                System.err.println("Unexpected error!");
2487                                ex.printStackTrace();
2488                            }
2489                        }
2490                        currentGizmo = board.checkoutGizmo(
2491                            (String)gizmosCombo.getSelectedItem());
2492                        boardView.setSelectedGizmo(currentGizmo);
2493                        if (board != null) {
2494                            boardView.setBoard(board);
2495                        }
2496                        boardView.redraw();
2497                    }
2498                });
2499    
2500                //rename.addActionListener(new ActionListener() {
2501                //    public void actionPerformed(ActionEvent e) {
2502                //        renameCurrentGizmo();
2503                //    }
2504                //});
2505    
2506                moveUp.addActionListener(new ActionListener() {
2507                    public void actionPerformed(ActionEvent e) {
2508                        moveCurrentGizmoUp();
2509                    }
2510                });
2511    
2512                moveLeft.addActionListener(new ActionListener() {
2513                    public void actionPerformed(ActionEvent e) {
2514                        moveCurrentGizmoLeft();
2515                    }
2516                });
2517    
2518                moveRight.addActionListener(new ActionListener() {
2519                    public void actionPerformed(ActionEvent e) {
2520                        moveCurrentGizmoRight();
2521                    }
2522                });
2523    
2524                moveDown.addActionListener(new ActionListener() {
2525                    public void actionPerformed(ActionEvent e) {
2526                        moveCurrentGizmoDown();
2527                    }
2528                });
2529    
2530                moveBackward.addActionListener(new ActionListener() {
2531                    public void actionPerformed(ActionEvent e) {
2532                        moveCurrentGizmoBackward();
2533                    }
2534                });
2535    
2536                moveForward.addActionListener(new ActionListener() {
2537                    public void actionPerformed(ActionEvent e) {
2538                        moveCurrentGizmoForward();
2539                    }
2540                });
2541    
2542                rotateClockwise.addActionListener(new ActionListener() {
2543                    public void actionPerformed(ActionEvent e) {
2544                        rotateCurrentGizmoClockwise();
2545                    }
2546                });
2547    
2548                rotateCounterclockwise.addActionListener(new ActionListener() {
2549                    public void actionPerformed(ActionEvent e) {
2550                        rotateCurrentGizmoCounterclockwise();
2551                    }
2552                });
2553    
2554                deleteGizmo.addActionListener(new ActionListener() {
2555                    public void actionPerformed(ActionEvent e) {
2556                        deleteCurrentGizmo();
2557                    }
2558                });
2559            }
2560    
2561            // Brings the list of Gizmos up to date. Clears the list and
2562            // repopulates it from the current Board.
2563            private void refreshGizmosCombo() {
2564                SortedSet<String> names = new TreeSet<String>(board.getNames());
2565                if (currentGizmo != null) {
2566                    names.add(currentGizmo.getName());
2567                }
2568                buildModePanel.gizmosCombo.removeAllItems();
2569                for (String name : names) {
2570                    if (name.equals("OuterWalls")) {
2571                        continue;
2572                    }
2573    
2574                    buildModePanel.gizmosCombo.addItem(name);
2575                }
2576            }
2577        }
2578    }
2579    
2580    
2581    
2582    /*
2583      Indentation spec for emacs:
2584      Local Variables:
2585      c-basic-offset:4
2586      End:
2587    */