001/**
002 * MainWindow -- The main document window for the GeomSS application.
003 * 
004 * Copyright (C) 2009-2024, by Joseph A. Huwaldt. All rights reserved.
005 * 
006 * This library is free software; you can redistribute it and/or modify it under the terms
007 * of the GNU Lesser General Public License as published by the Free Software Foundation;
008 * either version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
012 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
013 * 
014 * You should have received a copy of the GNU Lesser General Public License along with
015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place -
016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html
017 */
018package geomss.app;
019
020import bsh.EvalError;
021import bsh.Interpreter;
022import bsh.util.JConsole;
023import geomss.GeomSSApp;
024import geomss.GeomSSScene;
025import geomss.GeomSSUtil;
026import geomss.geom.GeomElement;
027import geomss.geom.GeomList;
028import geomss.geom.GeometryList;
029import geomss.geom.reader.GeomReader;
030import geomss.geom.reader.GeomReaderFactory;
031import geomss.geom.reader.XGSSGeomReader;
032import static geomss.j3d.ProjectionPolicy.PARALLEL_PROJECTION;
033import static geomss.j3d.ProjectionPolicy.PERSPECTIVE_PROJECTION;
034import geomss.j3d.RenderType;
035import geomss.ui.DialogItem;
036import geomss.ui.InputDialog;
037import jahuwaldt.io.ExtFilenameFilter;
038import jahuwaldt.io.FileUtils;
039import jahuwaldt.j3d.TransformChangeEvent;
040import jahuwaldt.j3d.TransformChangeListener;
041import jahuwaldt.j3d.image.JPEGImageObserver;
042import jahuwaldt.j3d.image.PNGImageObserver;
043import jahuwaldt.swing.*;
044import java.awt.*;
045import java.awt.event.ActionEvent;
046import java.awt.event.ActionListener;
047import java.awt.event.WindowEvent;
048import java.awt.print.PageFormat;
049import java.awt.print.PrinterException;
050import java.awt.print.PrinterJob;
051import java.io.File;
052import java.io.IOException;
053import java.net.URL;
054import java.text.MessageFormat;
055import java.util.ArrayList;
056import java.util.List;
057import static java.util.Objects.isNull;
058import static java.util.Objects.nonNull;
059import static java.util.Objects.requireNonNull;
060import java.util.ResourceBundle;
061import javax.swing.*;
062import javax.swing.text.DefaultEditorKit;
063import javolution.text.TypeFormat;
064import javolution.util.FastMap;
065
066
067/**
068 * Main window for the GeomSS program. Most of the program code is based here.
069 *
070 * <p> Modified by: Joseph A. Huwaldt </p>
071 *
072 * @author Joseph A. Huwaldt, Date: May 2, 2009
073 * @version January 1, 2024
074 */
075@SuppressWarnings("serial")
076public class MainWindow extends JFrame {
077
078    private static final short CANVAS_WIDTH = 640;          //  Initial 3D canvas width & height.
079    private static final short CANVAS_HEIGHT = 480;
080
081    //  Menu items for the File menu.
082    private static final int EXPORT_AS_ITEM = 9;
083
084    //  Menu items for the Edit menu.
085    private static final int CUT_ITEM = 3;
086    private static final int COPY_ITEM = 4;
087    private static final int PASTE_ITEM = 5;
088    
089    //  The page format used for printing.
090    private PageFormat _pf = null;
091
092    /**
093     * A count of the number of instances of the application window that have been opened.
094     */
095    private static int _instanceCount = 0;
096
097    //  A list of available point geometry writers.
098    private final List<GeomReader> _geomWriters = new ArrayList();
099
100    //  A reference to this application's 3D canvas.
101    private GeomSSCanvas3D _canvas;
102
103    //  An image observer that writes out PNG files.
104    private final PNGImageObserver _PNGObserver = new PNGImageObserver();
105
106    //  An image observer that writes out PNG files.
107    private final JPEGImageObserver _JPEGObserver = new JPEGImageObserver();
108
109    //  The BeanShell console for this window.
110    private final JConsole _console = new JConsole();
111
112    //  The BeanShell interpreter for this window.
113    private final Interpreter _bsh = new Interpreter(_console);
114
115    //  Reference to the last saved file.
116    private File _fileRef = null;
117
118    /**
119     * Constructor for our application window.
120     *
121     * @param name        The name of this window (usually file name of data file or
122     *                    "Untitled"). May not be null.
123     * @param newData     The data set this window contains. May not be null.
124     */
125    @SuppressWarnings("OverridableMethodCallInConstructor")
126    private MainWindow(String name, GeomElement newData) throws NoSuchMethodException {
127        super(requireNonNull(name));
128        requireNonNull(newData);
129
130        //  Have the window do nothing when it closes.
131        this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
132
133        //  Position the window automatically.
134        this.setLocationByPlatform(true);
135
136        //  Define the window icon in the taskbar.
137        URL imgURL = ClassLoader.getSystemResource(MDIApplication.getInstance().getResourceBundle().getString("applicationIconURL"));
138        Image image = Toolkit.getDefaultToolkit().getImage(imgURL);
139        if (nonNull(image))
140            this.setIconImage(image);
141
142        // Layout the display of this window.
143        initDisplay(newData);
144
145        //  Set up the menu bar.
146        JMenuBar menuBar = createMenuBar();
147        this.setJMenuBar(menuBar);
148
149        //  Pack the window.
150        this.pack();
151
152        //  Initialize the BeanShell interpreter.
153        initializeBeanShell();
154
155        // Increment our instance counter.
156        ++_instanceCount;
157    }
158
159    /**
160     * Initializes the BeanShell environment for this program.
161     */
162    private void initializeBeanShell() {
163
164        //  Start the BeanShell Interpreter.
165        new Thread(_bsh).start();
166
167        //  Initialize BeanShell
168        SwingUtilities.invokeLater(new Runnable() {
169            @Override
170            public void run() {
171                try {
172                    GeomSSUtil.setInterpreter(_bsh);
173
174                    //  Initialize BeanShell with our default imports, etc.
175                    ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
176                    GeomSSBatch.initializeBeanShell(resBundle.getString("appName"), _bsh, new PublicInterface());
177
178                    //  Set the working directory to the saved path.
179                    String path = MDIApplication.getInstance().getPreferences().getLastPath();
180                    if (nonNull(path))
181                        _bsh.eval("cd(\"" + path + "\");");
182
183                } catch (Exception e) {
184                    e.printStackTrace();
185                }
186            }
187        });
188
189    }
190
191    //-----------------------------------------------------------------------------------
192    /**
193     * Sets the title for this frame to the specified string. Also notifies the main
194     * application class so that the "Windows" menu gets updated too.
195     *
196     * @param title The title to be displayed in the frame's border.
197     */
198    @Override
199    public void setTitle(String title) {
200        super.setTitle(requireNonNull(title));
201        MDIApplication.windowTitleChanged(this);
202    }
203
204    /**
205     * Layouts the contents of this main application window.
206     *
207     * @param theData The data in this window.
208     */
209    private void initDisplay(GeomElement theData) throws NoSuchMethodException {
210
211        //  Get the content pane of this window.
212        Container cp = this.getContentPane();
213        cp.setLayout(new BorderLayout());
214
215        //  Create a 3D canvas to hold our model.
216        _canvas = new GeomSSCanvas3D(theData, CANVAS_WIDTH, CANVAS_HEIGHT);
217        _canvas.addCaptureObserver(_PNGObserver);
218        _canvas.addCaptureObserver(_JPEGObserver);
219
220        //  Add all the content of this window (other than the toolbar) to a split pane in the center.
221        JSplitPane content = new JSplitPane(JSplitPane.VERTICAL_SPLIT, _console, _canvas);
222        cp.add(content, BorderLayout.CENTER);
223        content.setOneTouchExpandable(true);
224//      content.setDividerLocation(CANVAS_HEIGHT);
225
226        //  Provide minimum sizes for the components.
227        _canvas.setMinimumSize(new Dimension(200, 200));
228        _canvas.setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
229        _console.setPreferredSize(new Dimension(CANVAS_WIDTH, 200));
230
231        //  Configure the BeanShell Console.
232        _console.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
233//      _console.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
234
235        //  Create a tool bar for the window and add it to an edge.
236        JToolBar toolbar = createToolbar();
237        cp.add(toolbar, BorderLayout.NORTH);
238
239    }
240
241    public GeomSSScene getScene() {
242        return _canvas.getScene();
243    }
244    
245    /**
246     * Initializes the menus associated with this window.
247     */
248    private JMenuBar createMenuBar() throws NoSuchMethodException {
249        MDIApplication guiApp = MDIApplication.getInstance();
250        ResourceBundle resBundle = guiApp.getResourceBundle();
251
252        JMenuBar menuBar = new JMenuBar();
253
254        // Set up the file menu.
255        List<String[]> menuStrings = new ArrayList();
256        String[] row = new String[3];
257        row[0] = resBundle.getString("newItemText");
258        row[1] = resBundle.getString("newItemKey");
259        row[2] = "handleNew";
260        menuStrings.add(row);
261        row = new String[3];
262        row[0] = resBundle.getString("openItemText");
263        row[1] = resBundle.getString("openItemKey");
264        row[2] = "handleOpen";
265        menuStrings.add(row);
266        row = new String[3];
267        row[0] = resBundle.getString("importItemText");
268        row[1] = resBundle.getString("importItemKey");
269        row[2] = "handleImport";
270        menuStrings.add(row);
271        row = new String[3];
272        row[0] = resBundle.getString("srcScriptItemText");
273        row[1] = null;
274        row[2] = "handleSrcScript";
275        menuStrings.add(row);
276        menuStrings.add(new String[3]); //  Blank line
277        row = new String[3];
278        row[0] = resBundle.getString("closeItemText");
279        row[1] = resBundle.getString("closeItemKey");
280        row[2] = "handleClose";
281        menuStrings.add(row);
282        row = new String[3];
283        row[0] = resBundle.getString("changeCWDItemText");
284        row[1] = null;
285        row[2] = "handleChangeCWD";
286        menuStrings.add(row);
287        row = new String[3];
288        row[0] = resBundle.getString("saveItemText");
289        row[1] = resBundle.getString("saveItemKey");
290        row[2] = "handleSave";
291        menuStrings.add(row);
292        row = new String[3];
293        row[0] = resBundle.getString("saveAsItemText");
294        row[1] = null;
295        row[2] = "handleSaveAs";
296        menuStrings.add(row);
297        row = new String[3];
298        row[0] = resBundle.getString("exportAsItemText");
299        row[1] = null;
300        row[2] = null;
301        menuStrings.add(row);
302        menuStrings.add(new String[3]); //  Blank line
303        row = new String[3];
304        row[0] = resBundle.getString("pageSetupItemText");
305        row[1] = null;
306        row[2] = "handlePageSetup";
307        menuStrings.add(row);
308        row = new String[3];
309        row[0] = resBundle.getString("printItemText");
310        row[1] = resBundle.getString("printItemKey");
311        row[2] = "handlePrint";
312        menuStrings.add(row);
313
314        JMenu menu = AppUtilities.buildMenu(this, resBundle.getString("fileMenuText"), menuStrings);
315        menuBar.add(menu);
316
317        //  Add the Export As sub-menu.
318        JMenu exportMenu = buildExportMenu(menu.getItem(EXPORT_AS_ITEM).getText());
319        if (nonNull(exportMenu)) {
320            menu.remove(EXPORT_AS_ITEM);
321            menu.add(exportMenu, EXPORT_AS_ITEM);
322        }
323
324        //  Add a Quit menu item (if it isn't automatically present).
325        if (!QuitJMenuItem.isAutomaticallyPresent()) {
326            menu.addSeparator();
327            menu.add(guiApp.getQuitJMenuItem());
328        }
329        
330        // Set up the edit menu.
331        menuStrings.clear();
332        row = new String[3];
333        row[0] = resBundle.getString("undoItemText");
334        row[1] = resBundle.getString("undoItemKey");
335        row[2] = null;
336        menuStrings.add(row);
337        row = new String[3];
338        row[0] = resBundle.getString("redoItemText");
339        row[1] = null;
340        row[2] = null;
341        menuStrings.add(row);
342        menuStrings.add(new String[3]); //  Blank line.
343        row = new String[3];
344        row[0] = resBundle.getString("cutItemText");
345        row[1] = resBundle.getString("cutItemKey");
346        row[2] = null;
347        menuStrings.add(row);
348        row = new String[3];
349        row[0] = resBundle.getString("copyItemText");
350        row[1] = resBundle.getString("copyItemKey");
351        row[2] = null;
352        menuStrings.add(row);
353        row = new String[3];
354        row[0] = resBundle.getString("pasteItemText");
355        row[1] = resBundle.getString("pasteItemKey");
356        row[2] = null;
357        menuStrings.add(row);
358        menu = AppUtilities.buildMenu(this, resBundle.getString("editMenuText"), menuStrings);
359
360        //  Add support for cut, copy & paste in all components.
361        TransferActionListener transferActionListener = new TransferActionListener();
362        setMenuItemAction(menu.getItem(CUT_ITEM), transferActionListener, new DefaultEditorKit.CutAction(),
363                (String)TransferHandler.getCutAction().getValue(Action.NAME));
364        setMenuItemAction(menu.getItem(COPY_ITEM), transferActionListener, new DefaultEditorKit.CopyAction(),
365                (String)TransferHandler.getCopyAction().getValue(Action.NAME));
366        setMenuItemAction(menu.getItem(PASTE_ITEM), transferActionListener, new DefaultEditorKit.PasteAction(),
367                (String)TransferHandler.getPasteAction().getValue(Action.NAME));
368
369        menuBar.add(menu);
370
371        //  Create a Window's menu.
372        menu = MDIApplication.newWindowsMenu(resBundle.getString("windowsMenuText"));
373        menuBar.add(menu);
374
375        //  Create an about menu item (if not automatically present).
376        if (!AboutJMenuItem.isAutomaticallyPresent()) {
377            //  Create Help menu.
378            menu = new JMenu(resBundle.getString("helpMenuText"));
379            menuBar.add(menu);
380            
381            //  Add the "About" item to the Help menu.
382            menu.add(guiApp.getAboutJMenuItem());
383        }
384
385        return menuBar;
386    }
387
388    /**
389     * Method that will build an "Export" menu containing a list of all the ModelWriter
390     * objects that are available.
391     *
392     * @param title The title for the menu that is created.
393     * @returns A JMenu instance containing a list of all ModelWriter objects that can
394     * write to files in various formats.
395     */
396    private JMenu buildExportMenu(String title) throws NoSuchMethodException {
397        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
398
399        //  Create the menu
400        JMenu menu = new JMenu(title);
401
402        //  Get the list of data readers that are available.
403        GeomReader[] allReaders = GeomReaderFactory.getAllReaders();
404
405        //  Loop over all the readers.
406        if (nonNull(allReaders)) {
407            for (GeomReader reader : allReaders) {
408
409                if (reader.canWriteData()) {
410                    //  Found a "writer".
411
412                    _geomWriters.add(reader);
413
414                    //  Create a menu item for it.
415                    JMenuItem menuItem = new JMenuItem(reader.toString() + "...");
416                    menuItem.addActionListener(new ExportAsMenuListener(reader));
417                    menuItem.setActionCommand(reader.toString());
418                    menuItem.setEnabled(true);
419
420                    //  Add the menu item to the menu.
421                    menu.add(menuItem);
422                }
423            }
424        }
425
426        //  Create an export to PNG item.
427        JMenuItem menuItem = new JMenuItem(resBundle.getString("exportAsPNGItemText"));
428        menuItem.addActionListener(AppUtilities.getActionListenerForMethod(this, "handleSaveAsPNG"));
429        menuItem.setEnabled(true);
430        menu.add(menuItem);
431
432        //  Create an export to JPEG item.
433        menuItem = new JMenuItem(resBundle.getString("exportAsJPEGItemText"));
434        menuItem.addActionListener(AppUtilities.getActionListenerForMethod(this, "handleSaveAsJPEG"));
435        menuItem.setEnabled(true);
436        menu.add(menuItem);
437
438        return menu;
439    }
440
441    /**
442     * Defines an action listener for our Export As menu items.
443     */
444    private class ExportAsMenuListener implements ActionListener {
445
446        //  Keep a reference to the reader used by this menu.
447        private final GeomReader _reader;
448
449        public ExportAsMenuListener(GeomReader reader) {
450            _reader = reader;
451        }
452
453        @Override
454        public void actionPerformed(ActionEvent evt) {
455            handleExportGeom(_reader);
456        }
457
458    }
459
460    /**
461     * Method that sets the specified action to a menu item while preserving the existing,
462     * item's text, accelerator key and mnemonic.
463     */
464    private void setMenuItemAction(JMenuItem item, TransferActionListener transferActionListener,
465            Action action, String actionCommand) {
466        String text = item.getText();
467        KeyStroke accel = item.getAccelerator();
468        int mnemonic = item.getMnemonic();
469        item.setAction(action);
470        item.setText(text);
471        item.setAccelerator(accel);
472        item.setMnemonic(mnemonic);
473        item.setActionCommand(actionCommand);
474
475        item.addActionListener(transferActionListener);
476    }
477
478    /**
479     * Create a tool bar for this window.
480     */
481    private JToolBar createToolbar() {
482        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
483        JToolBar toolbar = new JToolBar();
484
485        //  Create a "center" button that centers the view.
486        URL url = ClassLoader.getSystemResource(resBundle.getString("centerAndZoomIcon"));
487        JButton button = new JButton(new ImageIcon(url));
488        button.setToolTipText(resBundle.getString("centerandZoomToolTip"));
489        button.addActionListener(new ActionListener() {
490            @Override
491            public void actionPerformed(ActionEvent evt) {
492                GeomSSScene scene = _canvas.getScene();
493                scene.centerAndZoom();
494            }
495        });
496        toolbar.add(button);
497
498        //  Create a "view angle" pop-up combo-box.
499        //  Load icons for the various settings.
500        String[] iconFileNames = {
501            resBundle.getString("rightSideViewIcon"), resBundle.getString("leftSideViewIcon"),
502            resBundle.getString("topViewIcon"), resBundle.getString("bottomViewIcon"),
503            resBundle.getString("frontViewIcon"), resBundle.getString("rearViewIcon"),
504            resBundle.getString("topRightFrontViewIcon"), resBundle.getString("topLeftFrontViewIcon"),
505            resBundle.getString("topRightBackViewIcon"), resBundle.getString("topLeftBackViewIcon")
506        };
507        ImageIcon[] icons = new ImageIcon[iconFileNames.length];
508        for (int i = 0; i < iconFileNames.length; ++i) {
509            url = ClassLoader.getSystemResource(iconFileNames[i]);
510            icons[i] = new ImageIcon(url);
511        }
512
513        JComboBox viewOptions = new ViewAngleComboBox(icons);
514        viewOptions.setMaximumSize(viewOptions.getPreferredSize());
515        viewOptions.setToolTipText(resBundle.getString("selectViewToolTip"));
516        toolbar.add(viewOptions);
517
518        //  Create a symmetry button.
519        url = ClassLoader.getSystemResource(resBundle.getString("symmetryUnselectedIcon"));
520        JToggleButton togButton = new JToggleButton(new ImageIcon(url));
521        url = ClassLoader.getSystemResource(resBundle.getString("symmetrySelectedIcon"));
522        togButton.setSelectedIcon(new ImageIcon(url));
523        togButton.addActionListener(new ActionListener() {
524            @Override
525            public void actionPerformed(ActionEvent evt) {
526                JToggleButton btn = (JToggleButton)evt.getSource();
527                GeomSSScene scene = _canvas.getScene();
528                scene.setMirrored(btn.isSelected());
529            }
530        });
531        togButton.setToolTipText(resBundle.getString("symmetrySelectToolTip"));
532        toolbar.add(togButton);
533
534        //  Create the projection policy button.
535        url = ClassLoader.getSystemResource(resBundle.getString("projPolicyUnselectedIcon"));
536        togButton = new JToggleButton(new ImageIcon(url));
537        url = ClassLoader.getSystemResource(resBundle.getString("projPolicySelectedIcon"));
538        togButton.setSelectedIcon(new ImageIcon(url));
539        togButton.addActionListener(new ActionListener() {
540            @Override
541            public void actionPerformed(ActionEvent evt) {
542                JToggleButton btn = (JToggleButton)evt.getSource();
543                GeomSSScene scene = _canvas.getScene();
544                scene.setProjectionPolicy((btn.isSelected() ? PARALLEL_PROJECTION : PERSPECTIVE_PROJECTION));
545            }
546        });
547        togButton.setToolTipText(resBundle.getString("projPolicyToolTip"));
548        toolbar.add(togButton);
549
550        //  Create the render type options.
551        String[] renderIconNames = {resBundle.getString("solidWireframeIcon"), resBundle.getString("solidIcon"),
552            resBundle.getString("wireframeIcon"), resBundle.getString("stringsOnlyIcon"),
553            resBundle.getString("pointsOnlyIcon")};
554        icons = new ImageIcon[renderIconNames.length];
555        for (int i = 0; i < renderIconNames.length; ++i) {
556            url = ClassLoader.getSystemResource(renderIconNames[i]);
557            icons[i] = new ImageIcon(url);
558        }
559        JComboBox renderOptions = new JComboBox(icons);
560        renderOptions.addActionListener(new ActionListener() {
561            @Override
562            public void actionPerformed(ActionEvent evt) {
563                JComboBox cb = (JComboBox)evt.getSource();
564                RenderType[] renderOptions = RenderType.values();
565                int selected = cb.getSelectedIndex();
566                GeomSSScene scene = _canvas.getScene();
567                scene.setRenderType(renderOptions[selected]);
568            }
569        });
570        renderOptions.setMaximumSize(renderOptions.getPreferredSize());
571        renderOptions.setToolTipText(resBundle.getString("pointGeomRenderingToolTip"));
572        toolbar.add(renderOptions);
573
574        return toolbar;
575    }
576
577    /**
578     * A JComboBox that deselects the list whenever the orientation in the 3D canvas
579     * changes.
580     */
581    private class ViewAngleComboBox extends JComboBox {
582
583        //  The list of all possible view angles.
584
585        private GeomSSCanvas3D.PDViewAngle[] _vAngleOptions = GeomSSCanvas3D.PDViewAngle.values();
586
587        //  Flag used to indicate that the user has just selected an item.
588        private boolean isSelecting = false;
589
590        public ViewAngleComboBox(Object[] items) {
591            super(items);
592
593            //  Listen for selection changes and change the view appropriately.
594            this.addActionListener(new ActionListener() {
595                @Override
596                public void actionPerformed(ActionEvent evt) {
597                    int index = getSelectedIndex();
598                    if (index >= 0) {
599                        isSelecting = true;
600                        _canvas.setView(_vAngleOptions[index]);
601                        isSelecting = false;
602                    }
603                }
604            });
605
606            //  Listen for orientation changes and deselect the items when one happens.
607            _canvas.addTransformChangeListener(new TransformChangeListener() {
608                @Override
609                public void transformChanged(TransformChangeEvent e) {
610                    if (!isSelecting) {
611                        if (e.getType().equals(TransformChangeEvent.Type.ROTATE)) {
612                            setSelectedIndex(-1);   //  Deselect the items in the menu.
613                        }
614                    }
615                }
616            });
617
618        }
619    }
620
621    /**
622     * Creates, and initializes, a new application window with the specified content.
623     * Effectively creates a new instance of this program.
624     *
625     * @param name        The name of this window (usually file name of data file or
626     *                    "Untitled").
627     * @param newData     The data set this window contains. If null is passed, an
628     *                    empty/default/new data set is created.
629     * @return A new application window with the specified content.
630     * @throws java.lang.NoSuchMethodException If there was a problem constructing the
631     * window's action listeners.
632     */
633    public static MainWindow newAppWindow(String name, GeomElement newData)
634            throws NoSuchMethodException {
635
636        //  If a name wasn't provided, createa  default one.
637        if (isNull(name) || name.length() < 1) {
638            // Create a unique name for the new instance (an initial project name).
639            name = MDIApplication.getInstance().getResourceBundle().getString("untitled");
640            if (_instanceCount > 0)
641                name = name + _instanceCount;
642        }
643
644        // Create an instance of this window (with a default data set if one isn't provided).
645        MainWindow appFrame = new MainWindow(name,
646                (nonNull(newData) ? newData : GeomList.newInstance(name)));
647
648        //  Store the new data in the BeanShell environment.
649        if (nonNull(newData)) {
650            try {
651                appFrame._bsh.set("newData", newData);
652            } catch (Exception e) {
653                e.printStackTrace();    //  Can't happen.
654            }
655        }
656
657        return appFrame;
658    }
659
660    /**
661     * Handle the user choosing the "New..." from the File menu. Creates a new, blank,
662     * document window.
663     *
664     * @param event The action event that caused this method to be called.
665     * @return A new, empty, application window.
666     */
667    public MainWindow handleNew(ActionEvent event) {
668        return (MainWindow)(MDIApplication.getInstance().handleNew(event));
669    }
670
671    /**
672     * Handle the user choosing "Close" from the File menu. This implementation dispatches
673     * a "Window Closing" event to this window.
674     *
675     * @param event The action event that caused this method to be called. Ignored.
676     */
677    public void handleClose(ActionEvent event) {
678        this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
679    }
680
681    /**
682     * Handle the user choosing "Open..." from the File menu. Lets the user choose a data
683     * file and open it.
684     *
685     * @param event The action event that caused this method to be called. Ignored.
686     */
687    public void handleOpen(ActionEvent event) {
688        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
689
690        try {
691            //  Ask the user to select a native geometry file.
692            String extension = resBundle.getString("primaryExtension");
693            ExtFilenameFilter fnFilter = new ExtFilenameFilter();
694            fnFilter.addExtension(extension);
695            String dir;
696            if (isNull(_fileRef))
697                dir = MDIApplication.getInstance().getPreferences().getLastPath();
698            else
699                dir = _fileRef.getParent();
700            File theFile = AppUtilities.selectFile(this, FileDialog.LOAD, resBundle.getString("fileDialogLoad"),
701                    dir, "", fnFilter);
702            if (isNull(theFile))
703                return;
704
705            //  Pass the inputs to the console to load the file.
706            String cwd = (String)_bsh.get("bsh.cwd");   //  Get the current working directory.
707            String pathName;
708            if (theFile.getParent().equals(cwd))
709                pathName = theFile.getName();
710            else
711                pathName = theFile.getPath();
712            if (AppUtilities.isWindows())
713                pathName = pathName.replace("\\", "/");
714            String command = "open(pathToFile(\"" + pathName + "\"));";
715
716            // Print the command to the console as if we opened it through BSH.
717            _bsh.println(command);
718
719            //  Read in the geometry and any workspace variables.
720            //  This is being done rather than calling BSH to catch exceptions here.
721            XGSSGeomReader reader = new XGSSGeomReader();
722            FastMap<String, Object> workspace = (FastMap<String, Object>)reader.readWorkspace(theFile);
723
724            //  Store all the variables into the BeanShell worksapce.
725            try {
726                for (String varName : workspace.keySet())
727                    _bsh.set(varName, workspace.get(varName));
728            } finally {
729                FastMap.recycle(workspace);
730            }
731
732            // Change the name of this app's windows to match the new file name.
733            String fileName = theFile.getName();
734            fileName = fileName.substring(0, fileName.lastIndexOf("."));
735            this.setTitle(fileName);
736
737            //  Store a reference to the file.
738            _fileRef = theFile;
739            MDIApplication.getInstance().getPreferences().setLastPath(theFile.getParent());
740
741        } catch (Exception e) {
742            AppUtilities.showException(this, resBundle.getString("unexpectedTitle"),
743                    resBundle.getString("unexpectedMsg"), e);
744            e.printStackTrace();
745        }
746    }
747
748    /**
749     * Handle the user choosing "Import..." from the File menu. Lets the user choose a
750     * data file and open it.
751     *
752     * @param event The action event that caused this method to be called (ignored).
753     */
754    public void handleImport(ActionEvent event) {
755        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
756
757        try {
758            //  Build up the input dialog.
759            DialogItem item = new DialogItem(resBundle.getString("inputDlgListLabel") + " ",
760                    resBundle.getString("inputDlgListExample"));
761            List<DialogItem> itemList = new ArrayList();
762            itemList.add(item);
763            item = new DialogItem(resBundle.getString("inputDlgFileLabel") + " ",
764                    new File(MDIApplication.getInstance().getPreferences().getLastPath(),
765                            resBundle.getString("inputDlgFileExample")));
766            item.setLoadFile(true);
767            itemList.add(item);
768
769            //  Show the dialog.
770            InputDialog dialog = new InputDialog(this, resBundle.getString("inputDlgTitle"), "", itemList);
771            itemList = dialog.getOutput();
772            if (isNull(itemList))
773                return;
774
775            //  Get the user inputs.
776            String listName = (String)itemList.get(0).getElement();
777            File theFile = (File)itemList.get(1).getElement();
778            if (!theFile.exists()) {
779                String msg = MessageFormat.format(resBundle.getString("fileDoesntExistMsg"), theFile.getName());
780                JOptionPane.showMessageDialog(this, msg, resBundle.getString("ioErrorTitle"),
781                        JOptionPane.ERROR_MESSAGE);
782                return;
783            }
784
785            //  Pass the inputs to the console.
786            String cwd = (String)_bsh.get("bsh.cwd");   //  Get the current working directory.
787            String pathName;
788            if (theFile.getParent().equals(cwd))
789                pathName = theFile.getName();
790            else
791                pathName = theFile.getPath();
792            if (AppUtilities.isWindows())
793                pathName = pathName.replace("\\", "/");
794            String command = listName + " = readGeomFile(pathToFile(\"" + pathName + "\"));";
795
796            // Print the command to the console and then run it.
797            new BSHCommandRunner(command).start();
798
799        } catch (EvalError e) {
800            e.printStackTrace();
801
802        } catch (Exception e) {
803            AppUtilities.showException(null, resBundle.getString("unexpectedTitle"),
804                    resBundle.getString("unexpectedMsg"), e);
805            e.printStackTrace();
806        }
807
808    }
809
810    /**
811     * Method that reads in the specified file and creates a new window for displaying
812     * it's contents.
813     *
814     * @param parent  Parent frame for dialogs (null is fine).
815     * @param theFile The file to be loaded and displayed. If null is passed, this method
816     *                will do nothing.
817     * @throws java.lang.NoSuchMethodException if there was a problem constructing this
818     * window's action listeners.
819     */
820    public static void newWindowFromDataFile(Frame parent, File theFile)
821            throws NoSuchMethodException {
822
823        //  Read in the model data from the input file.
824        GeometryList newData = readGeometryData(parent, theFile);
825
826        if (nonNull(newData)) {
827
828            // Create an instance of an application window to get the program rolling.
829            MainWindow window = newAppWindow(theFile.getName(), newData);
830
831            //  Center the geometry in the window.
832            window._canvas.getScene().centerAndZoom();
833
834            //  Display the window.
835            window.setVisible(true);
836            MDIApplication.addWindow(window);
837
838        }
839
840    }
841
842    /**
843     * Method that reads in a data file and return the resulting data structure.
844     *
845     * @param parent    The parent frame for dialogs (null is fine).
846     * @param theFile   The file to be read in. If <code>null</code> is passed, this
847     *                  method will do nothing.
848     */
849    private static GeometryList readGeometryData(Component parent, File theFile) {
850        requireNonNull(theFile);
851        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
852        GeometryList data = null;
853
854        try {
855            data = GeomSSBatch.readGeometryData(resBundle, parent, theFile);
856
857        } catch (IOException e) {
858            String msg = e.getMessage();
859            if (isNull(msg)) {
860                if (nonNull(e.getCause()))
861                    msg = e.getCause().getMessage();
862            }
863            JOptionPane.showMessageDialog(parent, msg,
864                    resBundle.getString("ioErrorTitle"), JOptionPane.ERROR_MESSAGE);
865            e.printStackTrace();
866
867        } catch (Exception e) {
868            AppUtilities.showException(parent, resBundle.getString("unexpectedTitle"),
869                    resBundle.getString("unexpectedMsg"), e);
870            e.printStackTrace();
871        }
872
873        return data;
874    }
875
876    /**
877     * Handle the user choosing "Source Script..." from the File menu. Lets the user
878     * choose a script file and source it.
879     *
880     * @param event The action event that caused this method to be called (ignored).
881     */
882    public void handleSrcScript(ActionEvent event) {
883        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
884
885        try {
886            ExtFilenameFilter fnFilter = new ExtFilenameFilter();
887            fnFilter.addExtension("bsh");
888            String dir = MDIApplication.getInstance().getPreferences().getLastPath();
889            File theFile = AppUtilities.selectFile(this, FileDialog.LOAD, resBundle.getString("fileDialogLoad"),
890                    dir, null, fnFilter);
891            if (isNull(theFile))
892                return;     //  User canceled.
893
894            //  Pass the inputs to the console.
895            String cwd = (String)_bsh.get("bsh.cwd");   //  Get the current working directory.
896            String command;
897            if (theFile.getParent().equals(cwd))
898                command = "source(\"" + theFile.getName() + "\");";
899            else
900                command = "source(\"" + theFile.getPath() + "\");";
901            if (AppUtilities.isWindows())
902                command = command.replace("\\", "/");
903
904            // Print the command to the console and then run it.
905            new BSHCommandRunner(command).start();
906
907        } catch (Exception e) {
908            _bsh.error(e);
909            e.printStackTrace();
910        }
911    }
912
913    /**
914     * Handle the user choosing "Save" from the File menu. This saves the model to the
915     * last file used. Catches and displays to the user all exceptions.
916     *
917     * @param event The action event that caused this method to be called (ignored).
918     */
919    public void handleSave(ActionEvent event) {
920        try {
921            //  Go off and actually do the save.  Catch exceptions here.
922            doSave();
923
924        } catch (IOException e) {
925            String msg = e.getMessage();
926            if (isNull(msg)) {
927                Throwable cause = e.getCause();
928                if (nonNull(cause))
929                    msg = cause.getMessage();
930            }
931            ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
932            JOptionPane.showMessageDialog(this, msg, resBundle.getString("ioErrorTitle"),
933                    JOptionPane.ERROR_MESSAGE);
934
935        } catch (Exception e) {
936            ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
937            AppUtilities.showException(this, resBundle.getString("unexpectedTitle"),
938                    resBundle.getString("unexpectedMsg"), e);
939            e.printStackTrace();
940        }
941    }
942
943    /**
944     * Handle the user choosing "Save" from the File menu. This saves the model to the
945     * last file used. All exceptions are thrown.
946     *
947     * @return true if the user cancels, false if the model was saved.
948     */
949    private boolean doSave() throws IOException, EvalError {
950        if (isNull(_fileRef))
951            return doSaveAs();
952
953        if (canWriteFile(_fileRef)) {
954            File theFile = _fileRef;
955
956            //  Pass the inputs to the console to load the file.
957            String cwd = (String)_bsh.get("bsh.cwd");   //  Get the current working directory.
958            String pathName;
959            if (theFile.getParent().equals(cwd))
960                pathName = theFile.getName();
961            else
962                pathName = theFile.getPath();
963            if (AppUtilities.isWindows())
964                pathName = pathName.replace("\\", "/");
965            String command = "save(pathToFile(\"" + pathName + "\"));";
966
967            // Print the command to the console and then run it.
968            new BSHCommandRunner(command).start();
969        }
970
971        return false;
972    }
973
974    /**
975     * Handle the user choosing "Save As" from the File menu. This asks the user for input
976     * and then saves the model to a file.
977     *
978     * @param event The action event that caused this method to be called (ignored).
979     */
980    public void handleSaveAs(ActionEvent event) {
981        try {
982            //  Go off and actually do the Save As....  Catch exceptions here.
983            doSaveAs();
984
985        } catch (IOException e) {
986            String msg = e.getMessage();
987            if (isNull(msg)) {
988                Throwable cause = e.getCause();
989                if (nonNull(cause))
990                    msg = cause.getMessage();
991            }
992            ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
993            JOptionPane.showMessageDialog(this, msg, resBundle.getString("ioErrorTitle"),
994                    JOptionPane.ERROR_MESSAGE);
995
996        } catch (Exception e) {
997            ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
998            AppUtilities.showException(this, resBundle.getString("unexpectedTitle"),
999                    resBundle.getString("unexpectedMsg"), e);
1000            e.printStackTrace();
1001        }
1002
1003    }
1004
1005    /**
1006     * Handle the user choosing "Save As" from the File menu. This asks the user for input
1007     * and then saves all the geometry to the selected file. This method throws all
1008     * exceptions.
1009     *
1010     * @return true if the user cancels, false if the model was saved.
1011     */
1012    private boolean doSaveAs() throws IOException {
1013        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
1014
1015        FastMap<String, Object> vars = new FastMap();
1016        FastMap<String, GeomElement> geom = new FastMap();
1017        try {
1018            //  Extract the workspace variables into a pair of Maps.
1019            GeomSSBatch.extractVariableMaps(_bsh, geom, vars);
1020            if (geom.isEmpty() && vars.isEmpty())
1021                return false; //  No geometry to be saved.
1022
1023            //  Build the directory and filename to prompt the user with.
1024            String extension = resBundle.getString("primaryExtension");
1025            ExtFilenameFilter fnFilter = new ExtFilenameFilter();
1026            fnFilter.addExtension(extension);
1027            String dir;
1028            String fileName;
1029            if (nonNull(_fileRef)) {
1030                // Prompt the user with the previous name/path.
1031                dir = _fileRef.getPath();
1032                fileName = _fileRef.getName();
1033            } else {
1034                dir = (String)_bsh.get("bsh.cwd");  //  Get the current working directory.
1035                fileName = this.getTitle() + "." + extension;
1036            }
1037
1038            //  Ask the user to select a file for saving.
1039            File theFile = selectFile4Save(extension, fnFilter, dir, fileName);
1040
1041            if (canWriteFile(theFile)) {
1042                //  Pass the inputs to the console to load the file.
1043                String cwd = (String)_bsh.get("bsh.cwd");   //  Get the current working directory.
1044                String pathName;
1045                if (theFile.getParent().equals(cwd))
1046                    pathName = theFile.getName();
1047                else
1048                    pathName = theFile.getPath();
1049                if (AppUtilities.isWindows())
1050                    pathName = pathName.replace("\\", "/");
1051                String command = "save(pathToFile(\"" + pathName + "\"));";
1052
1053                // Print the command to the console as if we were going to run it directly.
1054                _bsh.println(command);
1055
1056                //  Write out the file.  This is done rather than going through BSH in order to catch
1057                //  exceptions here.
1058                XGSSGeomReader writer = new XGSSGeomReader();
1059                writer.write(theFile, geom, vars);
1060
1061                // Change the name of this app's windows to match the new file name.
1062                fileName = FileUtils.getFileNameWithoutExtension(theFile);
1063                this.setTitle(fileName);
1064
1065                //  Store off the file reference for later use.
1066                MDIApplication.getInstance().getPreferences().setLastPath(theFile.getParent());
1067                _fileRef = theFile;
1068
1069                return false;
1070            }
1071
1072        } catch (bsh.UtilEvalError | bsh.EvalError e) {
1073            throw new IOException(e);
1074        } finally {
1075            FastMap.recycle(vars);
1076            FastMap.recycle(geom);
1077        }
1078
1079        return true;
1080    }
1081
1082    /**
1083     * Handle the user choosing "Save As PNG..." from the File menu. This asks the user
1084     * for input and then saves the image to a file.
1085     *
1086     * @param event The action event that caused this method to be called (ignored).
1087     */
1088    public void handleSaveAsPNG(ActionEvent event) {
1089
1090        //  Build the directory and filename to prompt the user with.
1091        String extension = "png";
1092        String dir = MDIApplication.getInstance().getPreferences().getLastPath();
1093        String fileName = this.getTitle() + "." + extension;
1094        ExtFilenameFilter fnFilter = new ExtFilenameFilter(extension);
1095
1096        //  Ask the user to select a file for saving.
1097        File theFile = selectFile4Save(extension, fnFilter, dir, fileName);
1098
1099        if (canWriteFile(theFile)) {
1100            //  Wait for the canvas to finish rendering if necessary.
1101            _canvas.waitForRendering(200);
1102
1103            //  Notify the image capture listener  that an image needs to be saved.
1104            _PNGObserver.setFilename(theFile.getPath());
1105            _PNGObserver.setCaptureNextFrame();
1106            _canvas.getView().repaint();
1107        }
1108    }
1109
1110    /**
1111     * Handle the user choosing "Save As JPEG..." from the File menu. This asks the user
1112     * for input and then saves the image to a file.
1113     *
1114     * @param event The action event that caused this method to be called (ignored).
1115     */
1116    public void handleSaveAsJPEG(ActionEvent event) {
1117
1118        //  Build the directory and filename to prompt the user with.
1119        String extension = "jpeg";
1120        String dir = MDIApplication.getInstance().getPreferences().getLastPath();
1121        String fileName = this.getTitle() + "." + extension;
1122        ExtFilenameFilter fnFilter = new ExtFilenameFilter(extension);
1123        fnFilter.addExtension("jpg");
1124
1125        //  Ask the user to select a file for saving.
1126        File theFile = selectFile4Save(extension, fnFilter, dir, fileName);
1127
1128        if (canWriteFile(theFile)) {
1129            //  Wait for the canvas to finish rendering if necessary.
1130            _canvas.waitForRendering(200);
1131
1132            //  Notify the image capture listener  that an image needs to be saved.
1133            _JPEGObserver.setFilename(theFile.getPath());
1134            _JPEGObserver.setCaptureNextFrame();
1135            _canvas.getView().repaint();
1136        }
1137    }
1138
1139    /**
1140     * Asks the user to select a file for saving.
1141     *
1142     * @param fileType The file extension for the file being saved (example: "png" for a
1143     *                 PNG file).
1144     * @param fnFilter The file name filter to use.
1145     * @param dir      The name of the directory path to prompt the user with (the default
1146     *                 path).
1147     * @param fileName The name of the file to prompt the user with (the default file).
1148     * @return A reference to the selected file or <code>null</code> for no file selected
1149     *         (user canceled or error). Exceptions are handled in this method.
1150     */
1151    private File selectFile4Save(String extension, ExtFilenameFilter fnFilter, String dir, String fileName) {
1152        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
1153
1154        try {
1155            //  Ask the user to select a file for saving.
1156            String msg = MessageFormat.format(resBundle.getString("fileSaveDialog"), extension.toUpperCase());
1157            File theFile = AppUtilities.selectFile4Save(this, msg,
1158                    dir, fileName, fnFilter, extension,
1159                    resBundle.getString("fileExists"), resBundle.getString("warningTitle"));
1160
1161            if (nonNull(theFile)) {
1162                if (theFile.exists() && !theFile.canWrite())
1163                    throw new IOException(resBundle.getString("canNotWrite2File"));
1164
1165                return theFile;
1166            }
1167
1168        } catch (IOException e) {
1169            String msg = e.getMessage();
1170            if (isNull(msg)) {
1171                Throwable cause = e.getCause();
1172                if (nonNull(cause))
1173                    msg = cause.getMessage();
1174            }
1175            JOptionPane.showMessageDialog(this, msg, resBundle.getString("ioErrorTitle"),
1176                    JOptionPane.ERROR_MESSAGE);
1177
1178        } catch (Exception e) {
1179            e.printStackTrace();
1180            AppUtilities.showException(this, resBundle.getString("unexpectedTitle"),
1181                    resBundle.getString("unexpectedMsg"), e);
1182        }
1183
1184        return null;
1185    }
1186
1187    /**
1188     * Displays a message to the user if a file exists but can not be written to.
1189     *
1190     * @param theFile The file to test.
1191     * @return <code>true</code> if the program can write to the file or
1192     *         <code>false</code> if theFile is <code>null</code> or the file exists but
1193     *         can not be written to.
1194     */
1195    public boolean canWriteFile(File theFile) {
1196        if (isNull(theFile))
1197            return false;
1198        if (!theFile.exists() || theFile.canWrite())
1199            return true;
1200
1201        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
1202        JOptionPane.showMessageDialog(this, resBundle.getString("canNotWrite2File"),
1203                resBundle.getString("ioErrorTitle"), JOptionPane.ERROR_MESSAGE);
1204        return false;
1205    }
1206
1207    /**
1208     * Handle the user choosing to export to a GeomReader. Lets the user choose a data
1209     * file and open it.
1210     *
1211     * @param reader The reader to use when exporting geometry.
1212     */
1213    public void handleExportGeom(GeomReader reader) {
1214        requireNonNull(reader);
1215        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
1216
1217        try {
1218            //  Build up the input dialog.
1219            DialogItem item = new DialogItem(resBundle.getString("exptDlgListLabel") + " ",
1220                    resBundle.getString("exptDlgListExample"));
1221            List<DialogItem> itemList = new ArrayList();
1222            itemList.add(item);
1223            item = new DialogItem(reader.toString() + " " + resBundle.getString("exptDlgSaveLabel") + " ",
1224                    new File(MDIApplication.getInstance().getPreferences().getLastPath(),
1225                            resBundle.getString("inputDlgFileExample")));
1226            item.setLoadFile(false);
1227            item.setFileExtension(reader.getExtension());
1228            itemList.add(item);
1229
1230            //  Show the dialog.
1231            InputDialog dialog = new InputDialog(this, resBundle.getString("inputDlgTitle"), "", itemList);
1232            itemList = dialog.getOutput();
1233            if (isNull(itemList))
1234                return;
1235
1236            //  Get the user inputs.
1237            String listName = (String)itemList.get(0).getElement();
1238            File theFile = (File)itemList.get(1).getElement();
1239
1240            if (canWriteFile(theFile)) {
1241                //  Pass the inputs to the console.
1242                String cwd = (String)_bsh.get("bsh.cwd");   //  Get the current working directory.
1243                String writerName = reader.getClass().toString();
1244                writerName = writerName.substring(writerName.lastIndexOf(".") + 1);
1245                String pathName;
1246                if (theFile.getParent().equals(cwd))
1247                    pathName = theFile.getName();
1248                else
1249                    pathName = theFile.getPath();
1250                if (AppUtilities.isWindows())
1251                    pathName = pathName.replace("\\", "/");
1252                String command = "writeGeomFile(" + listName + ", pathToFile(\"" + pathName + "\"), new "
1253                        + writerName + "());";
1254                _bsh.eval(command);
1255                _bsh.println(command);
1256            }
1257
1258        } catch (EvalError e) {
1259            e.printStackTrace();
1260            AppUtilities.showException(this, resBundle.getString("evalErrTitle"),
1261                    resBundle.getString("unexpectedMsg"), e);
1262
1263        } catch (Exception e) {
1264            e.printStackTrace();
1265            AppUtilities.showException(this, resBundle.getString("unexpectedTitle"),
1266                    resBundle.getString("unexpectedMsg"), e);
1267        }
1268
1269    }
1270
1271    /**
1272     * Handle the user choosing "Change Working Directory..." from the File menu. Displays
1273     * a File selection dialog allowing the user to select the working directory.
1274     *
1275     * @param event The action event that caused this method to be called (ignored).
1276     */
1277    public void handleChangeCWD(ActionEvent event) {
1278        ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
1279
1280        //  Create a folder selection dialog asking the user to select a directory.
1281        FolderDialog fd = new FolderDialog(this, resBundle.getString("selectNewCWDText"));
1282        fd.setDirectory(MDIApplication.getInstance().getPreferences().getLastPath());
1283        fd.setVisible(true);
1284        if (nonNull(fd.getFile())) {
1285            //  The user has selected a directory.
1286            String newDir = fd.getDirectory();
1287
1288            //  MS Windows is a special case.
1289            if (AppUtilities.isWindows()) {
1290                newDir = newDir.replace("\\", "/");
1291            }
1292
1293            //  Change the CWD in BeanShell.
1294            try {
1295                String command = "cd(\"" + newDir + "\");";
1296                _bsh.eval(command);
1297                _bsh.println(command);
1298
1299                //  Save a reference to the current working directory so it can be restored later.
1300                MDIApplication.getInstance().getPreferences().setLastPath(newDir);
1301
1302            } catch (EvalError e) {
1303                String msg = e.getMessage();
1304                JOptionPane.showMessageDialog(this, msg, resBundle.getString("evalErrTitle"),
1305                        JOptionPane.ERROR_MESSAGE);
1306            }
1307        }
1308
1309    }
1310
1311    /**
1312     * Handle the user choosing "Page Setup..." from the File menu. Displays a Page Setup
1313     * dialog allowing the user to change the page settings.
1314     *
1315     * @param event The action event that caused this method to be called (ignored).
1316     */
1317    public void handlePageSetup(ActionEvent event) {
1318        PrinterJob job = PrinterJob.getPrinterJob();
1319        if (isNull(_pf))
1320            _pf = job.defaultPage();
1321        _pf = job.pageDialog(_pf);
1322    }
1323
1324    /**
1325     * Handle the user choosing "Print" from the File menu. Prints the currently displayed
1326     * plot.
1327     *
1328     * @param event The action event that caused this method to be called (ignored).
1329     */
1330    public void handlePrint(ActionEvent event) {
1331        PrinterJob job = PrinterJob.getPrinterJob();
1332
1333        if (isNull(_pf))
1334            _pf = job.defaultPage();
1335        job.setPrintable(_canvas, _pf);
1336
1337        if (job.printDialog()) {
1338            try {
1339                job.print();
1340
1341            } catch (PrinterException e) {
1342                JOptionPane.showMessageDialog(this, e);
1343            }
1344        }
1345
1346    }
1347
1348    /**
1349     * A thread for running BSH commands that could take some time to complete. This
1350     * prevents the GUI from being held up while the command is running.
1351     */
1352    private class BSHCommandRunner extends Thread {
1353
1354        private final String _command;
1355
1356        public BSHCommandRunner(String command) {
1357            _command = command;
1358        }
1359
1360        @Override
1361        public void run() {
1362            try {
1363                _bsh.println(_command);
1364                _bsh.eval(_command);
1365                _bsh.println("==> \"" + _command + "\" Done!");
1366            } catch (Exception e) {
1367                _bsh.error(e);
1368                e.printStackTrace();
1369            }
1370        }
1371    }
1372
1373    /**
1374     * A class that serves as the public interface (in BeanShell) for this application.
1375     */
1376    private class PublicInterface implements GeomSSApp {
1377
1378        /**
1379         * Return a reference to the 3D scene.
1380         */
1381        @Override
1382        public GeomSSScene getScene() {
1383            return _canvas.getScene();
1384        }
1385
1386        /**
1387         * Return the parent Frame for this application.
1388         */
1389        @Override
1390        public Frame getParentFrame() {
1391            return MainWindow.this;
1392        }
1393
1394        /**
1395         * Return a reference to the preferences for the GeomSS application.
1396         */
1397        @Override
1398        public Preferences getPreferences() {
1399            return MDIApplication.getInstance().getPreferences();
1400        }
1401
1402        /**
1403         * Return the resource bundle for this application containing the localized
1404         * application Strings.
1405         */
1406        @Override
1407        public ResourceBundle getResourceBundle() {
1408            return MDIApplication.getInstance().getResourceBundle();
1409        }
1410
1411        /**
1412         * Add the supplied window to the Windows menu of the main application. The window
1413         * should be made visible before calling this method to avoid an inconsistent user
1414         * interface state (a window listed in the "Windows" menu, but not visible). The
1415         * window will be removed from the "Windows" menu when it's "dispose" method is
1416         * called.
1417         *
1418         * @param window The window to be added to the Windows menu.
1419         */
1420        @Override
1421        public void addToWindowsMenu(Window window) {
1422            requireNonNull(window);
1423            MDIApplication.addWindow(window);
1424        }
1425
1426        /**
1427         * Quit or exit the application after properly saving preferences, etc.
1428         */
1429        @Override
1430        public void quit() {
1431            //  Save a reference to the current working directory so it can be restored later.
1432            try {
1433                String cwd = (String)_bsh.get("bsh.cwd");
1434                MDIApplication.getInstance().getPreferences().setLastPath(cwd);
1435            } catch (Exception e) {
1436                e.printStackTrace();    //  Can't happen.
1437            }
1438
1439            //  Ask Java to quit if the user selects this menu item.
1440            //  The quit handler (if one is registered) will be called automatically.
1441            System.exit(0);
1442        }
1443
1444        /**
1445         * Read in a geometry file and return a GeometryList instance. All exceptions are
1446         * handled by this method.
1447         *
1448         * @param theFile The file to be read in. If <code>null</code> is passed, this
1449         *                method will do nothing.
1450         * @return A GeometryList object containing the geometry read in from the file or
1451         *         <code>null</code> if the user cancels the read at any point or if an
1452         *         exception of any kind is thrown.
1453         */
1454        @Override
1455        public GeometryList readGeomFile(File theFile) {
1456            return readGeometryData(MainWindow.this, theFile);
1457        }
1458
1459        /**
1460         * Write out geometry to the specified file using the specified GeometryList
1461         * instance (some GeomReader classes have specific requirements for the contents
1462         * of this list). All exceptions are handled by this method.
1463         *
1464         * @param geometry The geometry object to be written out.
1465         * @param theFile  The file to be written to.
1466         * @param writer   The GeomReader to use to write out the file.
1467         */
1468        @Override
1469        public void writeGeomFile(GeometryList geometry, File theFile, GeomReader writer) {
1470            if (canWriteFile(theFile)) {
1471                ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
1472                try {
1473
1474                    //  Write the geometry to the file.
1475                    GeomSSBatch.writeGeometryData(resBundle, MainWindow.this, geometry, theFile, writer);
1476
1477                } catch (IOException e) {
1478                    String msg = e.getMessage();
1479                    if (isNull(msg)) {
1480                        if (nonNull(e.getCause()))
1481                            msg = e.getCause().getMessage();
1482                    }
1483                    JOptionPane.showMessageDialog(MainWindow.this, msg,
1484                            resBundle.getString("ioErrorTitle"), JOptionPane.ERROR_MESSAGE);
1485                    e.printStackTrace();
1486                }
1487            }
1488        }
1489
1490        /**
1491         * Returns a list of all known GeomReader objects that are capable of writing to a
1492         * file.
1493         */
1494        @Override
1495        public List<GeomReader> getAllGeomWriters() {
1496            return _geomWriters;
1497        }
1498
1499        /**
1500         * Save the entire global workspace to an XGSS file. All defined geometry and
1501         * non-geometry variables in the global workspace will be saved to the specified
1502         * file.
1503         *
1504         * @param theFile The file to write the workspace to in XGSS format.
1505         */
1506        @Override
1507        public void saveWorkspace(File theFile) {
1508            if (canWriteFile(theFile)) {
1509
1510                ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
1511                try {
1512                    //  Save the workspace to the specified file.
1513                    GeomSSBatch.saveWorkspace(resBundle, _bsh, theFile);
1514
1515                } catch (IOException e) {
1516                    String msg = e.getMessage();
1517                    if (isNull(msg)) {
1518                        if (nonNull(e.getCause()))
1519                            msg = e.getCause().getMessage();
1520                    }
1521
1522                    JOptionPane.showMessageDialog(MainWindow.this, msg, resBundle.getString("ioErrorTitle"),
1523                            JOptionPane.ERROR_MESSAGE);
1524                    e.printStackTrace();
1525
1526                } catch (Exception e) {
1527                    AppUtilities.showException(MainWindow.this, resBundle.getString("unexpectedTitle"),
1528                            resBundle.getString("unexpectedMsg"), e);
1529                    e.printStackTrace();
1530                }
1531            }
1532        }
1533
1534        /**
1535         * Load the specified XGSS file into the current global workspace. All the
1536         * geometry and non-geometry variables in the specified XGSS file will be loaded
1537         * into the current global workspace. Any existing variables with the same names
1538         * will be silently overwritten.
1539         *
1540         * @param theFile The XGSS file to load into the current workspace.
1541         */
1542        @Override
1543        public void loadWorkspace(File theFile) {
1544            requireNonNull(theFile);
1545            ResourceBundle resBundle = MDIApplication.getInstance().getResourceBundle();
1546            try {
1547                //  Load in the specified file.
1548                GeomSSBatch.loadWorkspace(resBundle, _bsh, theFile);
1549
1550            } catch (IOException e) {
1551                String msg = e.getMessage();
1552                if (isNull(msg)) {
1553                    if (nonNull(e.getCause()))
1554                        msg = e.getCause().getMessage();
1555                }
1556
1557                JOptionPane.showMessageDialog(MainWindow.this, msg, resBundle.getString("ioErrorTitle"),
1558                        JOptionPane.ERROR_MESSAGE);
1559                e.printStackTrace();
1560
1561            } catch (Exception e) {
1562                AppUtilities.showException(MainWindow.this, resBundle.getString("unexpectedTitle"),
1563                        resBundle.getString("unexpectedMsg"), e);
1564                e.printStackTrace();
1565            }
1566        }
1567
1568        /**
1569         * Save a copy of the current 3D view as a PNG file.
1570         */
1571        @Override
1572        public void saveAsPNG() {
1573            handleSaveAsPNG(null);
1574        }
1575
1576        /**
1577         * Save a copy of the current 3D view as a PNG file.
1578         *
1579         * @param file The file to be saved.
1580         */
1581        @Override
1582        public void saveAsPNG(File file) {
1583            if (canWriteFile(file)) {
1584                //  Wait for the canvas to finish rendering if necessary.
1585                _canvas.waitForRendering(200);
1586
1587                //  Notify the image capture listener  that an image needs to be saved.
1588                _PNGObserver.setFilename(file.getPath());
1589                _PNGObserver.setCaptureNextFrame();
1590                _canvas.getView().repaint();
1591            }
1592        }
1593
1594        /**
1595         * Save a copy of the current 3D view as a JPEG file.
1596         */
1597        @Override
1598        public void saveAsJPEG() {
1599            handleSaveAsJPEG(null);
1600        }
1601
1602        /**
1603         * Save a copy of the current 3D view as a JPEG file.
1604         *
1605         * @param file The file to be saved.
1606         */
1607        @Override
1608        public void saveAsJPEG(File file) {
1609            if (canWriteFile(file)) {
1610                //  Wait for the canvas to finish rendering if necessary.
1611                _canvas.waitForRendering(200);
1612
1613                //  Notify the image capture listener  that an image needs to be saved.
1614                _JPEGObserver.setFilename(file.getPath());
1615                _JPEGObserver.setCaptureNextFrame();
1616                _canvas.getView().repaint();
1617            }
1618        }
1619
1620        /**
1621         * Print the current 3D view. The user will be asked to supply information on the
1622         * print settings.
1623         */
1624        @Override
1625        public void print() {
1626            handlePrint(null);
1627        }
1628
1629        /**
1630         * Positions the input window at the location stored in the preferences using the
1631         * supplied key (with "PosX" and "PosY" appended). If the preference key isn't
1632         * found or if the returned values can not be turned into numbers, the
1633         * setLocationByPlatform() flag is set for the window.
1634         *
1635         * @param prefsKey The prefix for the preference key to use ("PosX" and "PosY"
1636         *                 will be appended in order to retrieve the actual preference
1637         *                 values).
1638         * @param window   The window to be positioned using the preference values.
1639         * @see Window#setLocationByPlatform(boolean) 
1640         */
1641        @Override
1642        public void posWindowFromPrefs(String prefsKey, Window window) {
1643            requireNonNull(prefsKey);
1644            requireNonNull(window);
1645            Preferences prefs = MDIApplication.getInstance().getPreferences();
1646            String windowPosX = prefs.get(prefsKey + "PosX");
1647            String windowPosY = prefs.get(prefsKey + "PosY");
1648            
1649            if (isNull(windowPosX) || isNull(windowPosY))
1650                //  No existing preference for the given key.
1651                window.setLocationByPlatform(true);
1652            
1653            else {
1654                try {
1655                    int x = TypeFormat.parseInt(windowPosX);
1656                    int y = TypeFormat.parseInt(windowPosY);
1657                    window.setLocation(x,y);
1658                } catch (NumberFormatException e) {
1659                    //  Fall back on the standard location.
1660                    window.setLocationByPlatform(true);
1661                }
1662            }
1663        }
1664
1665        /**
1666         * Save the location of the specified window in the application preferences using
1667         * the supplied key (with "PosX" and "PosY" appended).
1668         *
1669         * @param prefsKey The prefix for the preference key to use ("PosX" and "PosY"
1670         *                 will be appended in order to save the actual preference
1671         *                 values).
1672         * @param window   The window for which the position is to be saved in the
1673         *                 preferences.
1674         */
1675        @Override
1676        public void savePrefsWindowPos(String prefsKey, Window window) {
1677            requireNonNull(prefsKey);
1678            requireNonNull(window);
1679            Preferences prefs = MDIApplication.getInstance().getPreferences();
1680            Rectangle bounds = window.getBounds();
1681            prefs.set(prefsKey + "PosX", String.valueOf(bounds.x));
1682            prefs.set(prefsKey + "PosY", String.valueOf(bounds.y));
1683        }
1684
1685    }   //  end PublicInterface
1686}