001/* Top-level window for Ptolemy models with a menubar and status bar.
002
003 Copyright (c) 1998-2016 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026 */
027package ptolemy.actor.gui;
028
029import java.awt.Color;
030import java.awt.Component;
031import java.awt.FileDialog;
032import java.awt.Image;
033import java.awt.Toolkit;
034import java.awt.event.ActionEvent;
035import java.awt.event.ActionListener;
036import java.awt.event.KeyEvent;
037import java.awt.print.PrinterException;
038import java.io.File;
039import java.io.IOException;
040import java.lang.ref.WeakReference;
041import java.net.MalformedURLException;
042import java.net.URI;
043import java.net.URL;
044import java.util.Iterator;
045import java.util.List;
046import java.util.Vector;
047
048import javax.swing.AbstractButton;
049import javax.swing.Action;
050import javax.swing.JFileChooser;
051import javax.swing.JMenu;
052import javax.swing.JMenuItem;
053import javax.swing.JOptionPane;
054import javax.swing.KeyStroke;
055
056import ptolemy.actor.injection.PortablePlaceable;
057import ptolemy.data.BooleanToken;
058import ptolemy.data.Token;
059import ptolemy.data.expr.FileParameter;
060import ptolemy.data.expr.Parameter;
061import ptolemy.gui.JFileChooserBugFix;
062import ptolemy.gui.MemoryCleaner;
063import ptolemy.gui.PtGUIUtilities;
064import ptolemy.gui.StatusBar;
065import ptolemy.gui.Top;
066import ptolemy.gui.UndeferredGraphicalMessageHandler;
067import ptolemy.kernel.Entity;
068import ptolemy.kernel.util.IllegalActionException;
069import ptolemy.kernel.util.Instantiable;
070import ptolemy.kernel.util.InternalErrorException;
071import ptolemy.kernel.util.Nameable;
072import ptolemy.kernel.util.NamedObj;
073import ptolemy.kernel.util.StringAttribute;
074import ptolemy.moml.MoMLParser;
075import ptolemy.util.FileUtilities;
076import ptolemy.util.MessageHandler;
077import ptolemy.util.StringUtilities;
078
079///////////////////////////////////////////////////////////////////
080//// TableauFrame
081
082/**
083 This is a top-level window associated with a tableau that has
084 a menubar and status bar. Derived classes should add components
085 to the content pane using a line like:
086 <pre>
087 getContentPane().add(component, BorderLayout.CENTER);
088 </pre>
089 The base class provides generic features for menubars and toolbars,
090 and this class specializes the base class for Ptolemy II.
091 <p>
092 A help menu is provided with two entries, About and Help. In both
093 cases, an HTML file is opened.  The configuration can specify which
094 HTML file to open by containing an instance of FileParameter with
095 name "_about" or "_help".  The value of this attribute is a file
096 name (which may begin with the keywords $CLASSPATH or $PTII to
097 specify that the file is located relative to the CLASSPATH or to
098 the Ptolemy II installation directory).
099
100 @author Edward A. Lee
101 @version $Id$
102 @since Ptolemy II 1.0
103 @Pt.ProposedRating Green (eal)
104 @Pt.AcceptedRating Yellow (celaine)
105 */
106@SuppressWarnings("serial")
107public class TableauFrame extends Top {
108    /** Construct an empty top-level frame.
109     *  After constructing this, it is necessary
110     *  to call setVisible(true) to make the frame appear.
111     *  It may also be desirable to call centerOnScreen().
112     */
113    public TableauFrame() {
114        this(null);
115    }
116
117    /** Construct an empty top-level frame managed by the specified
118     *  tableau and the default status bar. After constructing this,
119     *  it is necessary to call setVisible(true) to make the frame appear.
120     *  It may also be desirable to call centerOnScreen().
121     *  @param tableau The managing tableau.
122     */
123    public TableauFrame(Tableau tableau) {
124        this(tableau, new StatusBar());
125    }
126
127    /** Construct an empty top-level frame managed by the specified
128     *  tableau with the specified status bar. After constructing this,
129     *  it is necessary to call setVisible(true) to make the frame appear.
130     *  It may also be desirable to call centerOnScreen().
131     *  @param tableau The managing tableau.
132     *  @param statusBar The status bar, or null to not include one.
133     */
134    public TableauFrame(Tableau tableau, StatusBar statusBar) {
135        super(statusBar);
136        // Set this frame in the tableau so that later in the constructor we can
137        // invoke tableau.getFrame() to get back this frame instead of null.
138        // -- tfeng (01/15/2009)
139        try {
140            if (tableau != null) {
141                tableau.setFrame(this);
142            }
143        } catch (IllegalActionException e) {
144            throw new InternalErrorException("This frame of class " + getClass()
145                    + " is not compatible with tableau " + tableau.getName());
146        }
147
148        setTableau(tableau);
149        setIconImage(_getDefaultIconImage());
150        _newMenuItems = new Vector<AbstractButton>();
151
152    }
153
154    /** Construct an empty top-level frame managed by the specified
155     *  tableau with the specified status bar and associated Placeable
156     *  object. Associating an instance of Placeable with this
157     *  frame has the effect that when this frame is closed,
158     *  if the placeable contains instances of WindowSizeAttribute
159     *  and/or SizeAttribute, then the window sizes are recorded.
160     *  After constructing this,
161     *  it is necessary to call setVisible(true) to make the frame appear.
162     *  It may also be desirable to call centerOnScreen().
163     *  @param tableau The managing tableau.
164     *  @param statusBar The status bar, or null to not include one.
165     *  @param placeable The associated Placeable.
166     */
167    public TableauFrame(Tableau tableau, StatusBar statusBar,
168            Placeable placeable) {
169        this(tableau, statusBar);
170        _placeable = placeable;
171    }
172
173    /** Construct an empty top-level frame managed by the specified
174     *  tableau with the specified status bar and associated PortablePlaceable
175     *  object. Associating an instance of PortablePlaceable with this
176     *  frame has the effect that when this frame is closed,
177     *  if the portablePlaceable contains instances of WindowSizeAttribute
178     *  and/or SizeAttribute, then the window sizes are recorded.
179     *  After constructing this,
180     *  it is necessary to call setVisible(true) to make the frame appear.
181     *  It may also be desirable to call centerOnScreen().
182     *  @param tableau The managing tableau.
183     *  @param statusBar The status bar, or null to not include one.
184     *  @param portablePlaceable The associated PortablePlaceable.
185     */
186    public TableauFrame(Tableau tableau, StatusBar statusBar,
187            PortablePlaceable portablePlaceable) {
188        this(tableau, statusBar);
189        _portablePlaceable = portablePlaceable;
190    }
191
192    ///////////////////////////////////////////////////////////////////
193    ////                         public methods                    ////
194
195    /** Get the alternative pack() interface for the ptolemy.gui.Top JFrame.
196     * @return the alternative pack() interface if one was set by the
197     * _alternateTopPackClass in the Configuration.  If one there is no TopPack,
198     * then return null.
199     * @see #pack()
200     */
201    public TopPack getAlternateTopPack() {
202        return _topPack;
203    }
204
205    /** Get the configuration at the top level of the hierarchy.
206     *  @return The configuration controlling this frame, or null
207     *   if there isn't one.
208     */
209    public Configuration getConfiguration() {
210        NamedObj tableau = getTableau();
211
212        if (tableau != null) {
213            NamedObj toplevel = tableau.toplevel();
214
215            if (toplevel instanceof Configuration) {
216                return (Configuration) toplevel;
217            }
218        }
219
220        return null;
221    }
222
223    /** Get the model directory in the top level configuration.
224     *  @return The model directory, or null if there isn't one.
225     */
226    public ModelDirectory getDirectory() {
227        Configuration configuration = getConfiguration();
228
229        if (configuration != null) {
230            return configuration.getDirectory();
231        } else {
232            return null;
233        }
234    }
235
236    /** Get the effigy for the model associated with this window.
237     *  @return The effigy for the model, or null if none exists.
238     */
239    public Effigy getEffigy() {
240        if (_tableau != null) {
241            return (Effigy) _tableau.getContainer();
242        }
243
244        return null;
245    }
246
247    /** Get the effigy for the specified Ptolemy model.
248     *  This searches all instances of PtolemyEffigy deeply contained by
249     *  the directory, and returns the first one it encounters
250     *  that is an effigy for the specified model.
251     *  @param model The model for which an effigy is desired.
252     *  @return The effigy for the model, or null if none exists.
253     */
254    public PtolemyEffigy getEffigy(NamedObj model) {
255        Configuration configuration = getConfiguration();
256
257        if (configuration != null) {
258            return configuration.getEffigy(model);
259        } else {
260            return null;
261        }
262    }
263
264    /** Get the tableau associated with this frame.
265     *  @return The tableau associated with this frame.
266     *  @see #setTableau(Tableau)
267     */
268    public Tableau getTableau() {
269        return _tableau;
270    }
271
272    /** Return true if the data associated with this window has been
273     *  modified since it was first read or last saved.  This returns
274     *  the value set by calls to setModified(), or false if that method
275     *  has not been called.
276     *  @return True if the data has been modified.
277     */
278    @Override
279    public boolean isModified() {
280        Effigy effigy = getEffigy();
281
282        if (effigy != null) {
283            return effigy.isModified();
284        } else {
285            return super.isModified();
286        }
287    }
288
289    /** Record whether the data associated with this window has been
290     *  modified since it was first read or last saved.  If you call
291     *  this with a true argument, then subsequent attempts to close
292     *  the window will trigger a dialog box to confirm the closing.
293     *  This overrides the base class to delegate to the effigy.
294     *  @param modified True if the data has been modified.
295     */
296    @Override
297    public void setModified(boolean modified) {
298        Effigy effigy = getEffigy();
299
300        if (effigy != null) {
301            effigy.setModified(modified);
302        }
303        super.setModified(modified);
304    }
305
306    /** Set the tableau associated with this frame.
307     *  @param tableau The tableau associated with this frame.
308     *  @see #getTableau()
309     */
310    public void setTableau(Tableau tableau) {
311        _tableau = tableau;
312    }
313
314    /**
315     * Optionally invoke an alternative pack() method.  If the
316     * _alternateTopPackClass attribute in the Configuration is set to
317     * the name of a class that implements the TopPack interface, then
318     * {@link ptolemy.actor.gui.TopPack#pack(Top, boolean)} is called.
319     * If the _alternateTopPackClass attribute is not set or set
320     * improperly, then Top.pack() is called from this method.
321     */
322    @Override
323    public void pack() {
324        super.pack();
325        Configuration configuration = getConfiguration();
326        // Check to see if we have a configuration because
327        // if we do "Listen to Actor", then getConfiguration()
328        // is returning null?
329        if (configuration != null) {
330            String alternateTopPackClass = "_alternateTopPackClass";
331            StringAttribute alternateTopPackClassAttribute = (StringAttribute) configuration
332                    .getAttribute(alternateTopPackClass);
333
334            // If the _alternateTopPackClass attribute is present,
335            // then we use the specified class to pack the gui
336            // if it is not set, just use Top.pack().
337
338            if (alternateTopPackClassAttribute != null) {
339                // Get the class that will build the library from the plugins
340                String topPackClassName = "";
341                try {
342                    topPackClassName = alternateTopPackClassAttribute
343                            .getExpression();
344                    if (topPackClassName == null) {
345                        throw new NullPointerException(
346                                "Null expression from the \""
347                                        + alternateTopPackClass
348                                        + "\" attribute?  It could be that "
349                                        + "the Configuration is not yet constructed?");
350                    }
351                    Class topPackClass = Class.forName(topPackClassName);
352                    if (topPackClass == null) {
353                        throw new ClassNotFoundException(
354                                "Failed to find class \"" + topPackClass
355                                        + "\", Class.forName() returned null.");
356                    }
357                    _topPack = (TopPack) topPackClass.newInstance();
358                    // Do the alternate pack
359                    _topPack.pack(this, _packCalled);
360                    _packCalled = true;
361                } catch (Exception ex) {
362                    throw new InternalErrorException(configuration, ex,
363                            "Could not get the alternate top pack class \""
364                                    + topPackClassName
365                                    + "\" named in the configuration by the \""
366                                    + alternateTopPackClass
367                                    + "\" attribute because: " + ex.getMessage()
368                                    + "\nPlease check your configuration and try again.");
369                }
370            } else {
371                super.pack();
372            }
373        }
374    }
375
376    /** If a PDF printer is available print to it.
377     *  @exception PrinterException If a printer with the string "PDF"
378     * cannot be found or if the job cannot be set to the PDF print
379     * service or if there is another problem printing.
380     */
381    public void printPDF() throws PrinterException {
382        _printPDF();
383    }
384
385    ///////////////////////////////////////////////////////////////////
386    ////                         public variables                  ////
387
388    /** The name of the default file to open when About is invoked.
389     *  This file should be relative to the home installation directory.
390     *  This file is used if the configuration does not specify an about file.
391     */
392    public String aboutFile = "ptolemy/configs/intro.htm";
393
394    /** The name of the default file to open when Help is invoked.
395     *  This file should be relative to the home installation directory.
396     *  This file is used if the configuration does not specify a help file.
397     */
398    public String helpFile = "ptolemy/configs/doc/basicHelp.htm";
399
400    ///////////////////////////////////////////////////////////////////
401    ////                         protected methods                 ////
402
403    /** Override the base class to open the intro.htm splash window,
404     *  which is in the directory ptolemy/configs.
405     */
406    @Override
407    protected void _about() {
408        // NOTE: We take some care here to ensure that this window is
409        // only opened once.
410        ModelDirectory directory = getDirectory();
411
412        if (directory != null) {
413            try {
414                Configuration configuration = getConfiguration();
415                FileParameter aboutAttribute = (FileParameter) configuration
416                        .getAttribute("_about", FileParameter.class);
417                URL doc;
418
419                if (aboutAttribute != null) {
420                    doc = aboutAttribute.asURL();
421                } else {
422                    doc = getClass().getClassLoader().getResource(aboutFile);
423                }
424
425                // The usual mechanism for opening a file is:
426                // configuration.openModel(null, doc, doc.toExternalForm());
427                // However, in this case, we want a smaller size, so we do
428                // something custom.
429                // Check to see whether the model is already open.
430                Effigy effigy = directory.getEffigy(doc.toExternalForm());
431
432                if (effigy == null) {
433                    // No main welcome window.  Create one.
434                    EffigyFactory effigyFactory = new HTMLEffigyFactory(
435                            directory.workspace());
436                    effigy = effigyFactory.createEffigy(directory, (URL) null,
437                            doc);
438
439                    effigy.identifier.setExpression(doc.toExternalForm());
440                    effigy.uri.setURL(doc);
441
442                    // If this fails, we do not want the effigy
443                    // in the directory.
444                    try {
445                        // Create a tableau if there is a tableau factory.
446                        TableauFactory factory = (TableauFactory) getConfiguration()
447                                .getAttribute("tableauFactory");
448
449                        if (factory != null) {
450                            Tableau tableau = factory.createTableau(effigy);
451
452                            if (tableau == null) {
453                                throw new Exception("Can't create Tableau.");
454                            }
455
456                            // The first tableau is a master.
457                            tableau.setMaster(true);
458
459                            // NOTE: This size is the same as what's in
460                            // the welcome window XML files in configs.
461                            tableau.size.setExpression("[650, 350]");
462                            tableau.show();
463                            return;
464                        }
465                    } catch (Exception ex) {
466                        // Remove effigy.
467                        effigy.setContainer(null);
468                    }
469                } else {
470                    // Model already exists.
471                    effigy.showTableaux();
472                    return;
473                }
474            } catch (Exception ex) {
475            }
476        }
477
478        // Don't report any errors.  Just use the default.
479        super._about();
480    }
481
482    /** Add a View menu and items to the File:New menu
483     *  if a tableau was given in the constructor.
484     *  <p>If the configuration has a _disableFileNew parameter that
485     *  is set to true, then we do not populate the File-&gt;New menu.
486     */
487    @Override
488    protected void _addMenus() {
489        super._addMenus();
490
491        if (_tableau != null) {
492            // Start with the File:New menu.
493            // Check to see if we have an effigy factory, and whether it
494            // is capable of creating blank effigies.
495            final Configuration configuration = getConfiguration();
496            if (configuration == null) {
497                System.out.println(
498                        "TableauFrame._addMenus: configuration == null?");
499                return;
500            }
501            EffigyFactory effigyFactory = (EffigyFactory) configuration
502                    .getEntity("effigyFactory");
503            boolean canCreateBlank = false;
504            final ModelDirectory directory = getDirectory();
505
506            // If the configuration has a _disableFileNew parameter that
507            // is set to true, then we do not populate the File->New menu.
508            boolean disableFileNew = false;
509            try {
510                Parameter disableFileNewParameter = (Parameter) configuration
511                        .getAttribute("_disableFileNew", Parameter.class);
512                if (disableFileNewParameter != null) {
513                    Token token = disableFileNewParameter.getToken();
514                    if (token instanceof BooleanToken) {
515                        disableFileNew = ((BooleanToken) token).booleanValue();
516                    }
517                }
518            } catch (Exception ex) {
519                // Ignore, there was a problem reading _disableFileNew,
520                // so we enable the File->New Menu choice
521            }
522
523            if (effigyFactory != null && directory != null && !disableFileNew) {
524                List factoryList = effigyFactory
525                        .entityList(EffigyFactory.class);
526                Iterator factories = factoryList.iterator();
527
528                boolean first = true;
529                while (factories.hasNext()) {
530                    final EffigyFactory factory = (EffigyFactory) factories
531                            .next();
532
533                    if (!factory.canCreateBlankEffigy()) {
534                        continue;
535                    }
536
537                    canCreateBlank = true;
538
539                    String name = factory.getName();
540                    ActionListener menuListener = new MenuItemListener(factory,
541                            directory, configuration);
542
543                    JMenuItem item = new JMenuItem(name);
544                    item.setActionCommand(name);
545                    item.setMnemonic(name.charAt(0));
546                    item.addActionListener(menuListener);
547                    _newMenuItems.addElement(item);
548                    if (first) {
549                        first = false;
550                        // From Daniel Crawl for Kepler
551                        item.setAccelerator(KeyStroke.getKeyStroke(
552                                KeyEvent.VK_N, Toolkit.getDefaultToolkit()
553                                        .getMenuShortcutKeyMask()));
554                    }
555                    ((JMenu) _fileMenuItems[_NEW_MENU_INDEX]).add(item);
556                }
557            }
558
559            if (canCreateBlank) {
560                // Enable the "New" item in the File menu.
561                _fileMenuItems[_NEW_MENU_INDEX].setEnabled(true);
562            }
563
564            // Next do the View menu.
565            Effigy tableauContainer = (Effigy) _tableau.getContainer();
566
567            if (tableauContainer != null) {
568                _factoryContainer = tableauContainer.getTableauFactory();
569
570                if (_factoryContainer != null) {
571                    // If setTableau() has been called on the effigy,
572                    // then there are multiple possible views of data
573                    // represented in this top-level window.
574                    // Thus, we create a View menu here.
575                    _viewMenu = new JMenu("View");
576                    _viewMenu.setMnemonic(KeyEvent.VK_V);
577                    _menubar.add(_viewMenu);
578
579                    ViewMenuListener viewMenuListener = new ViewMenuListener();
580                    Iterator factories = _factoryContainer
581                            .attributeList(TableauFactory.class).iterator();
582
583                    while (factories.hasNext()) {
584                        TableauFactory factory = (TableauFactory) factories
585                                .next();
586                        String name = factory.getName();
587                        JMenuItem item = new JMenuItem(name);
588
589                        // The "action command" is available to the listener.
590                        item.setActionCommand(name);
591                        item.setMnemonic(name.charAt(0));
592                        item.addActionListener(viewMenuListener);
593                        _viewMenu.add(item);
594                    }
595                }
596            }
597        }
598    }
599
600    /** Close the window.  Derived classes should override this to
601     *  release any resources or remove any listeners.  In this class,
602     *  if the data associated with this window have been modified,
603     *  and there are no other tableaux in the parent effigy or
604     *  any effigy that contains it,
605     *  then ask the user whether to save the data before closing.
606     *  @return False if the user cancels on a save query.
607     */
608    @Override
609    protected boolean _close() {
610        if (_debugClosing) {
611            System.out.println("TableauFrame._close() : " + this.getName());
612        }
613
614        // Record window properties, if appropriate.
615        if (_getPlaceable() instanceof NamedObj) {
616            Iterator properties = ((NamedObj) _getPlaceable())
617                    .attributeList(WindowPropertiesAttribute.class).iterator();
618            while (properties.hasNext()) {
619                WindowPropertiesAttribute windowProperties = (WindowPropertiesAttribute) properties
620                        .next();
621                windowProperties.recordProperties(this);
622            }
623            // Regrettably, have to also record the size of the contents
624            // because in Swing, setSize() methods do not set the size.
625            // Only the first component size is recorded.
626            properties = ((NamedObj) _getPlaceable())
627                    .attributeList(SizeAttribute.class).iterator();
628            while (properties.hasNext()) {
629                SizeAttribute size = (SizeAttribute) properties.next();
630                Component[] components = getContentPane().getComponents();
631                if (components.length > 0) {
632                    size.recordSize(components[0]);
633                }
634            }
635        }
636        boolean result = true;
637        // If we were given no tableau, then just close the window
638        if (getEffigy() == null) {
639            result = super._close();
640        } else {
641            Effigy masterEffigy = getEffigy().masterEffigy();
642
643            // If the top-level effigy has any open tableau that
644            // is not this one, and this one is not a master,
645            // then simply close.  No need to prompt
646            // for save, as that will be done when that tableau is closed.
647            if (!_tableau.isMaster()) {
648                List tableaux = masterEffigy.entityList(Tableau.class);
649                Iterator tableauxIterator = tableaux.iterator();
650
651                while (tableauxIterator.hasNext()) {
652                    Tableau tableau = (Tableau) tableauxIterator.next();
653
654                    if (!(tableau instanceof DialogTableau)
655                            && tableau != _tableau) {
656                        // NOTE: We use dispose() here rather than just hiding the
657                        // window.  This ensures that derived classes can react to
658                        // windowClosed events rather than overriding the
659                        // windowClosing behavior given here.
660                        dispose();
661                        _clearPlaceable();
662                        return true;
663                    }
664                }
665            }
666
667            // If we get here, there was no other tableau.
668            // NOTE: Do not use the superclass method so we can
669            // check for children of the model.
670            // NOTE: We use dispose() here rather than just hiding the
671            // window.  This ensures that derived classes can react to
672            // windowClosed events rather than overriding the
673            // windowClosing behavior given here.
674            if (isModified()) {
675
676                Effigy effigy = getEffigy();
677                if (!effigy.isPersistent()) {
678                    if (_debugClosing) {
679                        NamedObj model = ((PtolemyEffigy) effigy).getModel();
680                        System.out.println("TableauFrame._close(): model "
681                                + model.getFullName() + " has Effigy " + effigy
682                                + ", which is not persistent, so it will not be saved.");
683                    }
684                    dispose();
685                    return true;
686                }
687
688                int reply = _queryForSave();
689
690                if (reply == _DISCARDED || reply == _FAILED) {
691                    // If the model has children, then
692                    // issue a warning that those children will
693                    // persist.  Give the user the chance to cancel.
694                    if (!_checkForDerivedObjects()) {
695                        _clearPlaceable();
696                        return false;
697                    }
698                }
699
700                if (reply == _SAVED) {
701                    dispose();
702                } else if (reply == _DISCARDED) {
703                    dispose();
704
705                    // If the changes were discarded, then we want
706                    // to mark the model unmodified, so we don't get
707                    // asked again if somehow the model is re-opened.
708                    setModified(false);
709
710                    // Purge any record of the model, since we have
711                    // chosen to not save the changes, so the next time
712                    // this is opened, it should be read again from the file.
713                    try {
714                        MoMLParser.purgeModelRecord(masterEffigy.uri.getURL());
715                    } catch (MalformedURLException e) {
716                        // Ignore... Hopefully will be harmless.
717                    }
718                } else {
719                    result = false;
720                }
721            } else {
722                // Window is not modified, so just dispose.
723                dispose();
724            }
725        }
726        if (result == true) {
727            // If the user hit Cancel, do not clear the placeables.
728            _clearPlaceable();
729        }
730        return result;
731    }
732
733    /** Dispose of this frame.
734     *
735     *  <p>Override this dispose() method to unattach any listeners that may keep
736     *  this model from getting garbage collected.  This method invokes the
737     *  dispose() method of the superclass,
738     *  {@link ptolemy.gui.Top}.
739     */
740    @Override
741    public void dispose() {
742        if (_debugClosing) {
743            System.out.println("TableauFrame.dispose() : " + this.getName());
744        }
745
746        // Deal with view menu action listeners
747        /*int c =*/MemoryCleaner.removeActionListeners(_viewMenu);
748        //System.out.println("_viewMenu: "+c);
749
750        // Deal with new menu action listeners
751        //int i = 0;
752        for (AbstractButton newMenuButton : _newMenuItems) {
753            /*c =*/MemoryCleaner.removeActionListeners(newMenuButton);
754            //System.out.println("newMenuButton["+(i++)+"]: "+c);
755        }
756
757        // The size attribute is holding a reference to this frame
758        // with an attached listener.  Free the reference so this
759        // frame can be garbage collected.  Also, null the reference
760        // to the tableau that created this frame
761        if (_tableau != null) {
762            _tableau.size.setSize(null);
763            setTableau(null);
764        }
765
766        if (_placeable != null) {
767            _placeable = null;
768        }
769
770        super.dispose();
771    }
772
773    /** Confirm that writing the specified model to the specified file is OK.
774     *  In particular, if the file exists, ask the user whether it is OK
775     *  to overwrite. If there is an open model from the specified file,
776     *  determine whether it has been modified, and prompt to discard changes
777     *  if it has.  Close the previously open model. If the previously open
778     *  model on this file contains the specified model, the it is never
779     *  OK to do the write, so return false.
780     *  @param model The model to write to the file, or null specify
781     *   that this will be delegated to the effigy associated with this
782     *   tableau.
783     *  @param file The file to write to.
784     *  @return True if it is OK to write the model to the file.
785     *  @exception MalformedURLException  If the file cannot be converted
786     *  to a URL.
787     */
788    protected boolean _confirmFile(Entity model, File file)
789            throws MalformedURLException {
790        URL newURL = file.toURI().toURL();
791        String newKey = newURL.toExternalForm();
792        Effigy previousOpen = getDirectory().getEffigy(newKey);
793
794        // If there is a previous open, and it's not the same,
795        // then we need to close the previous.
796        // If we do save as to the same file, then we will get
797        // the current effigy, and we don't want to close it.
798        if (previousOpen != null && previousOpen != getEffigy()) {
799            // The destination file is already open.
800            // NOTE: If the model being saved is a submodel of the
801            // model associated with previousOpen, then we will close
802            // it before we save it, which will result in an error
803            // like "can't find an effigy to delegate writing to."
804            // I don't have a good workaround for this, so for now,
805            // disallow this type of save.  If the model argument
806            // is specified, then check it. Otherwise check the
807            // effigies.
808            boolean containmentError = false;
809
810            if (model != null) {
811                if (previousOpen instanceof PtolemyEffigy) {
812                    NamedObj possibleContainer = ((PtolemyEffigy) previousOpen)
813                            .getModel();
814
815                    if (possibleContainer != null
816                            && possibleContainer.deepContains(model)) {
817                        containmentError = true;
818                    }
819                }
820            } else {
821                if (previousOpen.deepContains(getEffigy())) {
822                    containmentError = true;
823                }
824            }
825
826            if (containmentError) {
827                MessageHandler.error("Cannot replace a model with a submodel."
828                        + " Please choose a different file name.");
829                return false;
830            }
831
832            if (previousOpen.isModified()) {
833                // Bring any visible tableaux to the foreground,
834                // then ask if it's OK to discard the changes?
835                previousOpen.showTableaux();
836
837                String confirm = "Unsaved changes in " + file.getName()
838                        + ". OK to discard changes?";
839
840                // Show a MODAL dialog
841                int selected = JOptionPane.showOptionDialog(this, confirm,
842                        "Discard changes?", JOptionPane.YES_NO_OPTION,
843                        JOptionPane.QUESTION_MESSAGE, null, null, null);
844
845                if (selected == 1) {
846                    return false;
847                }
848
849                // If the model has children, then
850                // issue a warning that those children will
851                // persist.  Give the user the chance to cancel.
852                if (!_checkForDerivedObjects()) {
853                    return false;
854                }
855
856                // Mark unmodified so that we don't get another
857                // query when it is closed.
858                previousOpen.setModified(false);
859            }
860
861            previousOpen.closeTableaux();
862        }
863
864        if (file.exists()) {
865            // Ask for confirmation before overwriting a file.
866            String query = "Overwrite " + file.getName() + "?";
867
868            // Show a MODAL dialog
869            int selected = JOptionPane.showOptionDialog(this, query,
870                    "Overwrite file?", JOptionPane.YES_NO_OPTION,
871                    JOptionPane.QUESTION_MESSAGE, null, null, null);
872
873            if (selected == 1) {
874                return false;
875            }
876        }
877
878        return true;
879    }
880
881    /** Close all open tableaux, querying the user as necessary to save data,
882     *  and then exit the application.  If the user cancels on any save,
883     *  then do not exit.
884     *  @see Tableau#close()
885     */
886    @Override
887    protected void _exit() {
888        ModelDirectory directory = getDirectory();
889
890        if (directory == null) {
891            return;
892        }
893
894        Iterator effigies = directory.entityList(Effigy.class).iterator();
895
896        while (effigies.hasNext()) {
897            Effigy effigy = (Effigy) effigies.next();
898
899            if (!effigy.closeTableaux()) {
900                // This is a hack because we don't want to change add a protected int _exit() method.
901                _exitResult = _CANCELED;
902                return;
903            }
904
905            try {
906                effigy.setContainer(null);
907            } catch (Exception ex) {
908                throw new InternalErrorException(
909                        "Unable to set effigy container to null! " + ex);
910            }
911        }
912
913        // Some of the effigies closed may have triggered other
914        // effigies being opened (if they were unnamed, and a saveAs()
915        // was triggered).  So we need to close those now.
916        // This is just a repeat of the above.
917        effigies = directory.entityList(Effigy.class).iterator();
918
919        while (effigies.hasNext()) {
920            Effigy effigy = (Effigy) effigies.next();
921
922            if (!effigy.closeTableaux()) {
923                return;
924            }
925
926            try {
927                effigy.setContainer(null);
928            } catch (Exception ex) {
929                throw new InternalErrorException(
930                        "Unable to set effigy container to null! " + ex);
931            }
932        }
933    }
934
935    /** Return the default icon image, or null if there is none.  Note
936     *  that Frame.setIconImage(null) will set the image to the
937     *  default platform dependent image.  If the configuration
938     *  contains a FileAttribute called _applicationIcon, then the
939     *  value of the _applicationIcon is used.  Otherwise, the default
940     *  value is ptolemy/actor/gui/PtolemyIISmallIcon.gif, which
941     *  is looked for in the classpath.
942     *  @return The default icon image, or null if there is none.
943     */
944    protected Image _getDefaultIconImage() {
945        if (_defaultIconImage == null) {
946            URL url = null;
947            try {
948                Configuration configuration = getConfiguration();
949                FileParameter iconAttribute = (FileParameter) configuration
950                        .getAttribute("_applicationIcon", FileParameter.class);
951
952                if (iconAttribute != null) {
953                    url = iconAttribute.asURL();
954                }
955            } catch (Throwable ex) {
956                // Ignore, we will set url to the default.
957            }
958            if (url == null) {
959                // Note that PtolemyIISmallIcon.gif is also in doc/img.
960                // We place a duplicate copy here to make it easy to ship
961                // jar files that contain all the appropriate images.
962                try {
963                    // Use nameToURL so this will work with WebStart
964                    // and so that this class can be extended and we
965                    // will still find the gif.
966                    url = FileUtilities.nameToURL(
967                            "$CLASSPATH/ptolemy/actor/gui/PtolemyIISmallIcon.gif",
968                            null, getClass().getClassLoader());
969                } catch (Throwable throwable) {
970                    // Ignore, stick with the default
971                }
972            }
973            if (url == null) {
974                return null;
975            }
976
977            // FIXME: For awhile under kepler if we had no _applicationIcon
978            // parameter and the PtolemyIISmallIcon.gif image was somewhere
979            // not in the ptolemy tree but still in the classpath, the
980            // icon would only partially render.  This could be because
981            // in Kepler, VergilApplication does not run everything in
982            // the Swing Event thread.  Nandita suggested using this as
983            // a workaround:
984            // setIconImage(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB));
985            // Another thing to check for is whether the icon is in the
986            // same directory as TableauFrame.class
987            Toolkit tk = Toolkit.getDefaultToolkit();
988            try {
989                _defaultIconImage = tk.createImage(url);
990            } catch (SecurityException ex) {
991                System.out.println("Warning: Could not read " + url
992                        + " for default icon of TableauFrame."
993                        + "(-sandbox always causes this)");
994            }
995        }
996
997        return _defaultIconImage;
998    }
999
1000    /** Get the name of this object, which in this class is the URI
1001     *  associated with the effigy, or the string "Unnamed" if none.
1002     *  This overrides the base class to provide a reasonable name
1003     *  for the title of the window.
1004     *  @return The name.
1005     */
1006    @Override
1007    protected String _getName() {
1008        Effigy effigy = getEffigy();
1009
1010        if (effigy != null) {
1011            URI uri = effigy.uri.getURI();
1012
1013            if (uri != null) {
1014                return uri.toString();
1015            }
1016        }
1017
1018        return "Unnamed";
1019    }
1020
1021    /** Display the help file given by the configuration, or if there is
1022     *  none, then the file specified by the public variable helpFile.
1023     *  To specify a default help file in the configuration, create
1024     *  a FileParameter named "_help" whose value is the name of the
1025     *  file.  If the specified file fails to open, then invoke the
1026     *  _about() method.
1027     *  @see FileParameter
1028     */
1029    @Override
1030    protected void _help() {
1031        try {
1032            Configuration configuration = getConfiguration();
1033            FileParameter helpAttribute = (FileParameter) configuration
1034                    .getAttribute("_help", FileParameter.class);
1035            URL doc;
1036
1037            if (helpAttribute != null) {
1038                doc = helpAttribute.asURL();
1039            } else {
1040                doc = getClass().getClassLoader().getResource(helpFile);
1041            }
1042
1043            configuration.openModel(null, doc, doc.toExternalForm());
1044        } catch (Exception ex) {
1045            _about();
1046        }
1047    }
1048
1049    /** Read the specified URL.  This delegates to the ModelDirectory
1050     *  to ensure that the preferred tableau of the model is opened, and
1051     *  that a model is not opened more than once.
1052     *  @param url The URL to read.
1053     *  @exception Exception If the URL cannot be read, or if there is no
1054     *   tableau.
1055     */
1056    @Override
1057    protected void _read(URL url) throws Exception {
1058        if (_tableau == null) {
1059            throw new Exception(
1060                    "No associated Tableau!" + " Can't open a file.");
1061        }
1062
1063        // NOTE: Used to use for the first argument the following, but
1064        // it seems to not work for relative file references:
1065        // new URL("file", null, _directory.getAbsolutePath()
1066        Nameable configuration = _tableau.toplevel();
1067
1068        if (configuration instanceof Configuration) {
1069            ((Configuration) configuration).openModel(url, url,
1070                    url.toExternalForm());
1071        } else {
1072            throw new InternalErrorException(
1073                    "Expected top-level to be a Configuration: "
1074                            + _tableau.toplevel().getFullName());
1075        }
1076    }
1077
1078    /** Save the model to the current file, determined by the
1079     *  <i>uri</i> parameter of the associated effigy, or if
1080     *  that has not been set or is not a writable file, or if the
1081     *  effigy has been set non-modifiable, then invoke
1082     *  _saveAs(). This calls _writeFile() to perform the save.
1083     *  @return True if the save succeeds.
1084     */
1085    @Override
1086    protected boolean _save() {
1087        if (_tableau == null) {
1088            throw new InternalErrorException(
1089                    "No associated Tableau! Can't save.");
1090        }
1091
1092        Effigy effigy = getEffigy();
1093        File file = effigy.getWritableFile();
1094
1095        if (!effigy.isModifiable() || file == null) {
1096            return _saveAs();
1097        } else {
1098            try {
1099                _writeFile(file);
1100                _updateHistory(file.getAbsolutePath(), false);
1101                setModified(false);
1102                return true;
1103            } catch (IOException ex) {
1104                report("Error writing file", ex);
1105                return false;
1106            }
1107        }
1108    }
1109
1110    /** Query the user for a filename, save the model to that file,
1111     *  and open a new window to view the model.
1112     *  This overrides the base class to update the entry in the
1113     *  ModelDirectory and to rename the model to match the file name.
1114     *  @return True if the save succeeds.
1115     */
1116    @Override
1117    protected boolean _saveAs() {
1118        return _saveAs(null);
1119    }
1120
1121    /** Query the user for a filename, save the model to that file,
1122     *  and open a new window to view the model.
1123     *  This overrides the base class to update the entry in the
1124     *  ModelDirectory and to rename the model to match the file name.
1125     *  @param extension If non-null, then the extension that is
1126     *  appended to the file name if there is no extension.
1127     *
1128     *  @return True if the save succeeds.
1129     */
1130    protected boolean _saveAs(String extension) {
1131        URL result = _saveAsHelper(extension);
1132        if (result == null) {
1133            return false;
1134        }
1135        return true;
1136    }
1137
1138    /** Query the user for a filename, save the model to that file,
1139     *  and open a new window to view the model.
1140     *
1141     *  @param extension If non-null, then the extension that is
1142     *  appended to the file name if there is no extension.
1143     *
1144     *  @return URL of the saved file if the save succeeds, null
1145     *  if save fails.
1146     */
1147    protected URL _saveAsHelper(String extension) {
1148        // _saveAsHelper is needed as part of Kepler.
1149
1150        if (_tableau == null) {
1151            throw new InternalErrorException(
1152                    "No associated Tableau! Can't save.");
1153        }
1154        if (PtGUIUtilities.useFileDialog()) {
1155            return _saveAsHelperFileDialog(extension);
1156        } else {
1157            return _saveAsHelperJFileChooser(extension);
1158        }
1159    }
1160
1161    /** Write the model to the specified file.  This method delegates
1162     *  to the effigy containing the associated Tableau, if there
1163     *  is one, and otherwise throws an exception.
1164     *  @param file The file to write to.
1165     *  @exception IOException If the write fails.
1166     */
1167    @Override
1168    protected void _writeFile(File file) throws IOException {
1169        Tableau tableau = getTableau();
1170
1171        if (tableau != null) {
1172            Effigy effigy = (Effigy) tableau.getContainer();
1173
1174            if (effigy != null) {
1175                // Ensure that if we do ever try to call this method,
1176                // that it is the top effigy that is written.
1177                effigy.writeFile(file);
1178                return;
1179            }
1180        }
1181
1182        throw new IOException("Cannot find an effigy to delegate writing.");
1183    }
1184
1185    ///////////////////////////////////////////////////////////////////
1186    ////                         protected variables               ////
1187
1188    /** The initial filename to use in the SaveAs dialog. */
1189    protected String _initialSaveAsFileName = null;
1190
1191    /** The view menu. Note that this is only created if there are multiple
1192     *  views, so if derived classes use it, they must test to see whether
1193     *  it is null.
1194     */
1195    protected JMenu _viewMenu;
1196
1197    ///////////////////////////////////////////////////////////////////
1198    ////                         private methods                   ////
1199
1200    /** If the model has children, then issue a warning that those
1201     *  children will persist in modified form.  Give the user the
1202     *  chance to cancel.
1203     *  @return False if there are children and
1204     *   the user cancels. True otherwise.
1205     */
1206    private boolean _checkForDerivedObjects() {
1207        Effigy effigy = getEffigy();
1208
1209        if (effigy instanceof PtolemyEffigy) {
1210            NamedObj model = ((PtolemyEffigy) effigy).getModel();
1211
1212            if (model instanceof Instantiable) {
1213                // NOTE: We are assuming that only the top-level
1214                // can defer outside the model. This is currently
1215                // true. Will it always be true?
1216                List children = ((Instantiable) model).getChildren();
1217
1218                if (children != null && children.size() > 0) {
1219                    StringBuffer confirm = new StringBuffer(
1220                            "Warning: This model defines a class, "
1221                                    + "and there are open models with instances\n"
1222                                    + "or subclasses the modified version of "
1223                                    + "this model:\n");
1224                    Iterator instances = children.iterator();
1225                    int length = confirm.length();
1226
1227                    while (instances.hasNext()) {
1228                        WeakReference reference = (WeakReference) instances
1229                                .next();
1230                        Instantiable instance = (Instantiable) reference.get();
1231
1232                        if (instance != null) {
1233                            confirm.append(instance.getFullName());
1234                            confirm.append(";");
1235
1236                            int newLength = confirm.length();
1237
1238                            if (newLength - length > 50) {
1239                                confirm.append("\n");
1240                                length = confirm.length();
1241                            }
1242                        }
1243                    }
1244
1245                    confirm.append("\nContinue?");
1246
1247                    // Show a MODAL dialog
1248                    int selected = JOptionPane.showOptionDialog(this, confirm,
1249                            "Warning: Instances or Subclasses",
1250                            JOptionPane.YES_NO_OPTION,
1251                            JOptionPane.QUESTION_MESSAGE, null, null, null);
1252
1253                    if (selected == 1) {
1254                        return false;
1255                    }
1256                }
1257            }
1258        }
1259
1260        return true;
1261    }
1262
1263    /** Save a file.  This method is called by {@link #_saveAsHelperFileDialog(String)}
1264     *  and {@link #_saveAsHelperJFileChooser(String)} so as to avoid code duplication.
1265     *  @param file The selected file to be saved.
1266     *  @param directory The directory of the file to be saved.
1267     *  @return URL of the saved file if the save succeeds, null
1268     *  if save fails.
1269     */
1270    private URL _saveAsHelperCommon(File file, File directory) {
1271        try {
1272            //if (!_confirmFile(null, file)) {
1273            //    return null;
1274            //}
1275
1276            URL newURL = file.toURI().toURL();
1277            String newKey = newURL.toExternalForm();
1278
1279            // Only set the directory if the user optionally confirmed the overwrite.
1280            _directory = directory;
1281            _writeFile(file);
1282            _updateHistory(file.getAbsolutePath(), false);
1283
1284            // The original file will still be open, and has not
1285            // been saved, so we do not change its modified status.
1286            // setModified(false);
1287            // Open a new window on the model.
1288            Tableau newTableau = getConfiguration().openModel(newURL, newURL,
1289                    newKey);
1290
1291            if (newTableau != null && newTableau.getFrame() != null) {
1292                newTableau.getFrame().setTitle(StringUtilities.abbreviate(
1293                        new File(_directory, file.getName()).toString()));
1294
1295                // If the tableau was unnamed before, then we need
1296                // to close this window after doing the save.
1297                Effigy effigy = getEffigy();
1298
1299                // If the effigy is "Unnamed", then don't call effigy.setContainer(null).
1300                // To replicate this:
1301                // Create an Unnamed editor: $PTII/bin/vergil, File -> New -> Graph Editor
1302                // Save to foo.xml
1303                // Close foo.xml
1304                // In the Unnamed editor, File -> Open foo.xml
1305                // _read() will throw "Expected top-level to be a Configuration"
1306                //
1307                // if (effigy != null) {
1308                //     String id = effigy.identifier.getExpression();
1309
1310                //     if (id.equals("Unnamed")) {
1311                //         // This will have the effect of closing all the
1312                //         // tableaux associated with the unnamed model.
1313                //         effigy.setContainer(null);
1314                //     }
1315                // }
1316
1317                // Change r61021 "dispose the old frame after opening a new one on call to saveAs"
1318                // resulted in vergil exiting if one does
1319                // 1. cd $PTII/ptolemy/domains/sdf/demo/Butterfly
1320                // 2. $PTII/bin/vergil Butterfly.xml
1321                // 3. File -> Save As, select Butterfly.xml, hit OK.
1322                // 4. When the "Ok to overwrite" window comes up, hit Yes.
1323                // 5. The vergil process exits.
1324                // So, we only call dispose if the tableaus are different.
1325
1326                // Check that _tableau is not null because running
1327                // ptolemy/actor/gt/demo/ConstOptimization/ConstOptimization.xml
1328                // and saving BaseOptimization.xml could result in an NPE.
1329                // NO: Leave the old tableau open on Save As.
1330                // Failing to do this can sometimes make the old tableau impossible
1331                // to open again, perhaps because of memory leak cleaning in dispose().
1332                /*
1333                if (_tableau != null && !_tableau.equals(newTableau)) {
1334                    dispose();
1335                }
1336                */
1337            }
1338            return newURL;
1339        } catch (Exception ex) {
1340            report("Error in save as.", ex);
1341            return null;
1342        }
1343    }
1344
1345    /** Query the user for a filename, save the model to that file,
1346     *  and open a new window to view the model.  This method
1347     *  uses java.awt.FileDialog and is usually used under Mac OS X.
1348     *  See {@link ptolemy.gui.PtGUIUtilities#useFileDialog()} for information
1349     *  on how to set a Java property to control whether FileDialog or
1350     *  JFileChooser is used.
1351     *
1352     *  @param extension If non-null, then the extension that is
1353     *  appended to the file name if there is no extension.
1354     *
1355     *  @return URL of the saved file if the save succeeds, null
1356     *  if save fails.
1357     */
1358    private URL _saveAsHelperFileDialog(String extension) {
1359        // This is odd, we need to replicate similar code from
1360        // Top._saveAsFileDialogImplementation()?  Why?
1361        // _saveAsHelper() is needed as part of Kepler.
1362
1363        FileDialog fileDialog = _saveAsFileDialogComponent();
1364
1365        if (fileDialog == null) {
1366            // Action was canceled, perhaps by Save As in a submodel.
1367            return null;
1368        }
1369
1370        if (_initialSaveAsFileName != null) {
1371            // Call setFile() with just the file name so that we don't
1372            // get the path, which is too much information.
1373
1374            // FIXME: should we call setDirectory() or will
1375            // the FileDialog use the user.dir Java property?
1376
1377            // fileDialog.setFile(new File(fileDialog
1378            // .getDirectory(), _initialSaveAsFileName).toString());
1379
1380            fileDialog.setFile(_initialSaveAsFileName);
1381        }
1382        fileDialog.show();
1383
1384        String selectedFile = fileDialog.getFile();
1385        // If the user selected "Cancel", then selectedFile will be null.
1386        if (selectedFile != null) {
1387            // fileDialog.getFile() *sometimes* returns an old, krufty
1388            // Mac OS 9 colon separated path.  Why?
1389            File file = null;
1390            if (selectedFile.startsWith(":")) {
1391                // FIXME: _directory is ignored?
1392                // To get selectedFile to contain colons, try Save As, then, in the
1393                // "Save As" entry widget, replace just the filename (the text after
1394                // the last slash) with new text.  For example, If I do Save As and
1395                // the entry widget says "/Users/cxh/ptII/foo", then change it
1396                // to say "/Users/cxh/ptII/bar".
1397                file = new File(selectedFile.replace(':', '/'));
1398            } else {
1399                // FIXME: Are there any circumstances under which selectedFile
1400                // will contain a colon as a directory separator but not
1401                // start with one?  Who knows?  Apple does, but there is no
1402                // documentation about this and Apple bugs are not world readable.
1403                //file = new File(_directory, selectedFile);
1404                // The user may have selected a different directory, so
1405                // get the directory from the FileDialog, but don't set
1406                // _directory until we have called _confirm().
1407                file = new File(fileDialog.getDirectory(), selectedFile);
1408            }
1409            if (extension != null && file.getName().indexOf(".") == -1) {
1410                // if the user has not given the file an extension, add it
1411                file = new File(file.getAbsolutePath() + extension);
1412            }
1413
1414            return _saveAsHelperCommon(file,
1415                    new File(fileDialog.getDirectory()));
1416        }
1417        // The user hit cancel or there was an error, so we did not
1418        // successfully save.
1419        return null;
1420    }
1421
1422    /** Query the user for a filename, save the model to that file,
1423     *  and open a new window to view the model.  This method uses
1424     *  javax.swing.JFileChooser and is usually used under
1425     *  non-Mac OS X platforms such as Windows or Linux.
1426     *  See {@link ptolemy.gui.PtGUIUtilities#useFileDialog()} for information
1427     *  on how to set a Java property to control whether FileDialog or
1428     *  JFileChooser is used.
1429     *
1430     *  @param extension If non-null, then the extension that is
1431     *  appended to the file name if there is no extension.
1432     *
1433     *  @return URL of the saved file if the save succeeds, null
1434     *  if save fails.
1435     */
1436    private URL _saveAsHelperJFileChooser(String extension) {
1437        // This is odd, we need to replicate similar code from
1438        // Top._saveAsJFileChooserImplementation()?  Why?
1439        // Answer: Because _saveAsHelper() is needed as part of Kepler.
1440
1441        // Swap backgrounds and avoid white boxes in "common places" dialog
1442        JFileChooserBugFix jFileChooserBugFix = new JFileChooserBugFix();
1443        Color background = null;
1444        try {
1445            background = jFileChooserBugFix.saveBackground();
1446            // Use the strategy pattern here to create the actual
1447            // dialog so that subclasses can customize this dialog.
1448            JFileChooser fileDialog = _saveAsJFileChooserComponent();
1449            if (_initialSaveAsFileName != null) {
1450                fileDialog.setSelectedFile(
1451                        new File(fileDialog.getCurrentDirectory(),
1452                                _initialSaveAsFileName));
1453            }
1454
1455            // Show the dialog.
1456            int returnVal = fileDialog.showSaveDialog(this);
1457            if (returnVal == JFileChooser.APPROVE_OPTION) {
1458                File file = fileDialog.getSelectedFile();
1459                if (extension != null && file.getName().indexOf(".") == -1) {
1460                    // if the user has not given the file an extension, add it
1461                    file = new File(file.getAbsolutePath() + extension);
1462                }
1463
1464                try {
1465                    // FileDialog asks about overwriting a file, FileChooser
1466                    // does not.
1467                    if (!_confirmFile(null, file)) {
1468                        return null;
1469                    }
1470                } catch (Throwable throwable) {
1471                    throw new RuntimeException(
1472                            "Failed to confirm saving of " + file, throwable);
1473                }
1474                return _saveAsHelperCommon(file,
1475                        fileDialog.getCurrentDirectory());
1476            }
1477
1478            // The user hit cancel or there was an error, so we did not
1479            // successfully save.
1480            return null;
1481        } finally {
1482            jFileChooserBugFix.restoreBackground(background);
1483        }
1484    }
1485
1486    /** Clear placeable and portablePlaceable by passing a null container.
1487     *
1488     */
1489    private void _clearPlaceable() {
1490        if (_placeable != null) {
1491            _placeable.place(null);
1492        }
1493        if (_portablePlaceable != null) {
1494            _portablePlaceable.place(null);
1495        }
1496    }
1497
1498    /** Get the appropriate instance of instance of Placeable or PortablePlaceable,
1499     *  depending upon what is initialized.
1500     *
1501     *  @return Instance of Placeable or PortablePlaceable
1502     */
1503    private Object _getPlaceable() {
1504        return _placeable != null ? _placeable : _portablePlaceable;
1505    }
1506
1507    ///////////////////////////////////////////////////////////////////
1508    ////                         private variables                 ////
1509    // The container of view factories, if one has been found.
1510    private TableauFactory _factoryContainer = null;
1511
1512    // The tableau that created this frame.
1513    private Tableau _tableau = null;
1514
1515    // The singleton icon image used for all ptolemy frames.
1516    private static Image _defaultIconImage = null;
1517
1518    /** Associated placeable. */
1519    private Placeable _placeable;
1520
1521    /** Associated portablePlaceable. */
1522    private PortablePlaceable _portablePlaceable;
1523
1524    /** Set to true when the pack() method is called.  Used by TopPack.pack(). */
1525    private boolean _packCalled = false;
1526
1527    /** Set in pack() if an alternate topPack is used. */
1528    protected TopPack _topPack = null;
1529
1530    /** A vector to keep track of ActionListeners on menu items. */
1531    private Vector<AbstractButton> _newMenuItems;
1532
1533    ///////////////////////////////////////////////////////////////////
1534    ////                         inner classes                     ////
1535
1536    /** A Listener for menu items. */
1537    protected static class MenuItemListener implements ActionListener {
1538        // FindBugs indicates that this should be a static class.
1539        /**
1540         * Constructs a MenuItemListener object.
1541         *
1542         * @param factory  The factor for the Effigy.
1543         * @param directory The directory of all the models.
1544         * @param configuration The Configuration for this instance
1545         * of Ptolemy.
1546         */
1547        public MenuItemListener(EffigyFactory factory, ModelDirectory directory,
1548                Configuration configuration) {
1549            _factory = factory;
1550            _directory = directory;
1551            _configuration = configuration;
1552        }
1553
1554        @Override
1555        public void actionPerformed(ActionEvent event) {
1556            Effigy effigy = null;
1557
1558            try {
1559                effigy = _factory.createEffigy(_directory);
1560            } catch (Exception ex) {
1561                MessageHandler.error("Could not create new effigy", ex);
1562            }
1563
1564            _configuration.createPrimaryTableau(effigy);
1565        }
1566
1567        private EffigyFactory _factory;
1568        private ModelDirectory _directory;
1569        private Configuration _configuration;
1570    }
1571
1572    /** Listener for view menu commands. */
1573    class ViewMenuListener implements ActionListener {
1574        @Override
1575        public void actionPerformed(ActionEvent e) {
1576            // Make this the default context for modal messages.
1577            UndeferredGraphicalMessageHandler.setContext(TableauFrame.this);
1578            if (_factoryContainer != null) {
1579                JMenuItem target = (JMenuItem) e.getSource();
1580                String actionCommand = null;
1581                Action action = target.getAction();
1582                if (action != null) {
1583
1584                    //the following should be OK because
1585                    //GUIUtilities.addMenuItem() automatically adds
1586                    //each incoming JMenuItems as a property of the
1587                    //Action itself - see
1588                    //diva.gui.GUIUtilities.addMenuItem(), line 202,
1589                    //ans so does kepler/src/exp/ptolemy/vergil/basic/
1590                    //BasicGraphFrame.storeSubMenus() line 2519...
1591
1592                    JMenuItem originalMenuItem = (JMenuItem) action
1593                            .getValue("menuItem");
1594                    if (originalMenuItem != null) {
1595                        actionCommand = originalMenuItem.getActionCommand();
1596                    } else {
1597                        actionCommand = target.getActionCommand();
1598                    }
1599                } else {
1600                    actionCommand = target.getActionCommand();
1601                }
1602                TableauFactory factory = (TableauFactory) _factoryContainer
1603                        .getAttribute(actionCommand);
1604                if (factory != null) {
1605                    Effigy tableauContainer = (Effigy) _tableau.getContainer();
1606
1607                    try {
1608                        Tableau tableau = factory
1609                                .createTableau(tableauContainer);
1610                        if (tableau == null) {
1611                            MessageHandler.warning(
1612                                    "Cannot create view. Perhaps the model needs to be saved first?");
1613                        } else {
1614                            tableau.show();
1615                        }
1616                    } catch (Throwable throwable) {
1617                        // Copernicus might throw a java.lang.Error if
1618                        // jhdl.Main cannot be resolved
1619                        MessageHandler.error("Cannot create view", throwable);
1620                    }
1621                }
1622            }
1623
1624            // NOTE: The following should not be needed, but jdk1.3beta
1625            // appears to have a bug in swing where repainting doesn't
1626            // properly occur.
1627            repaint();
1628        }
1629    }
1630}