001/* Top-level window 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.gui;
028
029import java.awt.BorderLayout;
030import java.awt.Color;
031import java.awt.Component;
032import java.awt.Dimension;
033import java.awt.EventQueue;
034import java.awt.FileDialog;
035import java.awt.Frame;
036import java.awt.KeyboardFocusManager;
037import java.awt.Toolkit;
038import java.awt.Window;
039import java.awt.event.ActionEvent;
040import java.awt.event.ActionListener;
041import java.awt.event.KeyEvent;
042import java.awt.event.WindowAdapter;
043import java.awt.event.WindowEvent;
044import java.awt.event.WindowFocusListener;
045import java.awt.print.PageFormat;
046import java.awt.print.Pageable;
047import java.awt.print.Printable;
048import java.awt.print.PrinterException;
049import java.awt.print.PrinterJob;
050import java.io.BufferedReader;
051import java.io.File;
052import java.io.FileReader;
053import java.io.FileWriter;
054import java.io.FilenameFilter;
055import java.io.IOException;
056import java.lang.reflect.Field;
057import java.net.URL;
058import java.util.ArrayList;
059import java.util.Collections;
060import java.util.Iterator;
061import java.util.LinkedList;
062import java.util.List;
063import java.util.Map;
064import java.util.Timer;
065import java.util.TimerTask;
066import java.util.WeakHashMap;
067
068import javax.print.PrintService;
069import javax.print.attribute.Attribute;
070import javax.print.attribute.HashPrintRequestAttributeSet;
071import javax.print.attribute.PrintRequestAttributeSet;
072import javax.print.attribute.standard.Destination;
073import javax.swing.AbstractAction;
074import javax.swing.JFileChooser;
075import javax.swing.JFrame;
076import javax.swing.JMenu;
077import javax.swing.JMenuBar;
078import javax.swing.JMenuItem;
079import javax.swing.JOptionPane;
080import javax.swing.KeyStroke;
081import javax.swing.SwingUtilities;
082import javax.swing.filechooser.FileFilter;
083
084import ptolemy.util.MessageHandler;
085import ptolemy.util.StatusHandler;
086import ptolemy.util.StringUtilities;
087
088///////////////////////////////////////////////////////////////////
089//// Top
090
091/**
092 This is a top-level window with a menubar and an optional status bar.
093 Derived classes should add components to the content pane using a
094 line like:
095 <pre>
096 getContentPane().add(component, BorderLayout.CENTER);
097 </pre>
098 Derived classes may wish to modify the menus.  The File
099 and Help menus are exposed as protected members.
100 The File menu items in the _fileMenuItems protected array are,
101 in order, Open File, Open URL, New, Save, Save As, Print, Close, and Exit.
102 The Help menu items in the _helpMenuItems protected array are,
103 in order, About and Help.
104 <p>
105 A derived class can use the insert() methods of JMenu
106 to insert a menu item defined by an Action or a JMenuItem
107 into a specified position in the menu.
108 Derived classes can also insert separators using the
109 insertSeparator() method of JMenu.
110 In principle, derived classes can also remove menu items
111 using the remove() methods of JMenu; however, we discourage this.
112 A basic principle of user interface design is habituation, where
113 there is considerable value in having menus that have consistent
114 contents and layout throughout the application (Microsoft, for
115 example, violates this principle with adaptive menus).
116 <p>
117 Instead of removing items from the menu, they can be disabled.
118 For example, to disable the "Save" item in the File menu, do
119 <pre>
120 _fileMenuItems[3].setEnabled(false);
121 </pre>
122 <p>
123 Some menu items are provided, but are disabled by default.
124 The "New" item, for example, can be enabled with
125 <pre>
126 _fileMenuItems[2].setEnabled(true);
127 </pre>
128 A derived class that enables this menu item can populate the menu with
129 submenu items.  This particular entry in the _fileMenuItems[2]
130 is a JMenu, not just a JMenuItem, so it can have menu items
131 added to it.
132 <p>
133 A derived class can add an entirely new menu (many do that).
134 However, at this time, the JMenuBar interface does not support
135 putting a new menu into an arbitrary position.  For this reason,
136 derived classes should insert new menus into the menu bar only
137 in the _addMenus() protected method.  This ensures that the File
138 menu is always the rightmost menu, and the Help menu is always
139 the leftmost menu.  The _addMenus() method is called when the window
140 is first packed.
141
142 @author Edward A. Lee and Steve Neuendorffer
143 @version $Id$
144 @since Ptolemy II 1.0
145 @Pt.ProposedRating Yellow (eal)
146 @Pt.AcceptedRating Yellow (janneck)
147 */
148@SuppressWarnings("serial")
149public abstract class Top extends JFrame
150        implements WindowFocusListener, StatusHandler {
151
152    /** Construct an empty top-level frame with the default status
153     *  bar.  After constructing this, it is necessary to call
154     *  pack() to have the menus added, and then setVisible(true)
155     *  to make the frame appear.  It may also be desirable to
156     *  call centerOnScreen().  This can be done after
157     *  pack() and before setVisible().
158     */
159    public Top() {
160        this(new StatusBar());
161    }
162
163    /** Construct an empty top-level frame with the specified status
164     *  bar.  After constructing this,
165     *  it is necessary to call pack() to have the menus added, and
166     *  then setVisible(true) to make the frame appear.  It may also
167     *  be desirable to call centerOnScreen().  This can be done after
168     *  pack() and before setVisible().
169     *  @param statusBar A status bar, or null to not insert one.
170     */
171    public Top(StatusBar statusBar) {
172        super();
173
174        _statusBar = statusBar;
175
176        // Ensure that user is prompted before closing if the data
177        // has been modified.
178        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
179        addWindowListener(new CloseWindowAdapter());
180
181        getContentPane().setLayout(new BorderLayout());
182
183        _fileMenuListener = new FileMenuListener();
184        _helpMenuListener = new HelpMenuListener();
185        _historyMenuListener = new HistoryMenuListener();
186
187        // Make this the default context for modal messages.
188        UndeferredGraphicalMessageHandler.setContext(this);
189
190        // If we are on a Mac, initialize the quit menu
191        // so that Command-Q saves data.
192        if (PtGUIUtilities.macOSLookAndFeel()) {
193            _macInitializer();
194        }
195
196        addWindowFocusListener(this);
197    }
198
199    ///////////////////////////////////////////////////////////////////
200    ////                         public methods                    ////
201
202    /** Open a dialog with basic information about this window.
203     *  Derived classes may override {@link #_about()}.
204     */
205    public final void about() {
206        // Under Mac OS X, the "About" menu choice invokes this method.
207        _about();
208    }
209
210    /** Center the window on the screen.  This must be called after the
211     *  window is populated with its contents, since it depends on the size
212     *  being known. If this method is called from a thread that is not
213     *  the AWT event dispatch thread, then its execution is deferred
214     *  and performed in that thread.
215     */
216    public void centerOnScreen() {
217        Runnable doCenter = new CenterOnScreenRunnable();
218
219        deferIfNecessary(doCenter);
220    }
221
222    /** Close the window, prompting the user to save changes if there
223     *  have been any.  Derived classes should override the protected
224     *  method _close(), not this one. This method returns immediately
225     *  if it is called outside the swing UI thread, deferring the action
226     *  so that it is executed in the swing thread.
227     */
228    public final void close() {
229        if (_debugClosing) {
230            System.out.println("Top.close() : " + this.getName());
231        }
232
233        Runnable doClose = new CloseWindowRunnable();
234
235        deferIfNecessary(doClose);
236    }
237
238    /** If this method is called in the AWT event dispatch thread,
239     *  then simply execute the specified action.  Otherwise,
240     *  if there are already deferred actions, then add the specified
241     *  one to the list.  Otherwise, create a list of deferred actions,
242     *  if necessary, and request that the list be processed in the
243     *  event dispatch thread.
244     *  <p>
245     *  Note that it does not work nearly as well to simply schedule
246     *  the action yourself on the event thread because if there are a
247     *  large number of actions, then the event thread will not be able
248     *  to keep up.  By grouping these actions, we avoid this problem.
249     *  @param action The Runnable object to execute.
250     */
251    public static void deferIfNecessary(Runnable action) {
252        // NOTE: This is a static version of a method in PlotBox, but
253        // we do not want to create cross dependencies between these
254        // packages.
255        // In swing, updates to showing graphics must be done in the
256        // event thread.  If we are in the event thread, then proceed.
257        // Otherwise, queue a request or add to a pending request.
258        if (EventQueue.isDispatchThread()) {
259            action.run();
260        } else {
261            synchronized (_deferredActions) {
262                // Add the specified action to the list of actions to perform.
263                _deferredActions.add(action);
264
265                // If it hasn't already been requested, request that actions
266                // be performed in the event dispatch thread.
267                if (!_actionsDeferred) {
268                    Runnable doActions = new DeferredActionsRunnable();
269
270                    // NOTE: Using invokeAndWait() here risks causing
271                    // deadlock.  Don't do it!
272                    SwingUtilities.invokeLater(doActions);
273                    _actionsDeferred = true;
274                }
275            }
276        }
277    }
278
279    /** Dispose of this frame.
280     *     Override this dispose() method to unattach any listeners that may keep
281     *  this model from getting garbage collected.  This method invokes the
282     *  dispose() method of the superclass,
283     *  {@link javax.swing.JFrame}.
284     */
285    @Override
286    public void dispose() {
287        if (_debugClosing) {
288            System.out.println("Top.dispose() : " + this.getName());
289        }
290
291        removeWindowFocusListener(this);
292
293        // Deal with help menu action listeners
294        /*int c =*/MemoryCleaner.removeActionListeners(_historyMenu);
295        //System.out.println("_historyMenu: "+c);
296        if (_historyMenu != null) {
297            _historyMenusAndListeners.remove(_historyMenu);
298        }
299        _historyMenuListener = null;
300
301        // Don't call removeWindowListeners here because Tableau.setFrame()
302        // adds a WindowListener to windowsClosed events and that listener
303        // handles closing the plots associated with a model.
304        //MemoryCleaner.removeWindowListeners(this);
305
306        // Deal with file menu action listeners
307        /*c =*/MemoryCleaner.removeActionListeners(_fileMenu);
308        //System.out.println("_fileMenu: "+c);
309        for (JMenuItem menuItem : _fileMenuItems) {
310            /*c =*/MemoryCleaner.removeActionListeners(menuItem);
311            //System.out.println("_fileMenuItems["+i+"]: "+c);
312        }
313        _fileMenuListener = null;
314
315        // Deal with help menu action listeners
316        /*c =*/MemoryCleaner.removeActionListeners(_helpMenu);
317        //System.out.println("_helpMenu: "+c);
318        for (JMenuItem menuItem : _helpMenuItems) {
319            /*c =*/MemoryCleaner.removeActionListeners(menuItem);
320            //System.out.println("_helpMenuItems["+i+"]: "+c);
321        }
322        _helpMenuListener = null;
323
324        /*c =*/MemoryCleaner.removeActionListeners(_menubar);
325        //System.out.println("_menubar: "+c);
326        _menubar.removeAll();
327
328        // ensure reference to this is removed
329        UndeferredGraphicalMessageHandler.setContext(null);
330
331        // I'm not sure exactly why this works but it does!
332        // I think it has to do with the KeyboardFocusManager
333        // holding onto the last focused component, so clearing and
334        // cycling seems to free up the reference to this window.
335        KeyboardFocusManager focusManager = KeyboardFocusManager
336                .getCurrentKeyboardFocusManager();
337        focusManager.clearGlobalFocusOwner();
338        focusManager.downFocusCycle();
339
340        // Avoid a leak under RHEL and Java 1.8.0_55
341        // See https://wiki.eecs.berkeley.edu/ptexternal/Main/Main/MemoryLeaks#LinuxBltSubRegion
342        // http://oracle.developer-works.com/article/5359339/How+to+realy+unregister+listeners+%28or+how+%22great%22+swing+leaks+memory%29
343        focusManager.setGlobalCurrentFocusCycleRoot(null);
344
345        // Set any AbstractActions to null.  This is not strictly necessary,
346        // but it helps free up memory more quickly.  By doing this here,
347        // we no longer require people to update dispose() by hand.
348        Class myClass = getClass();
349        // Loop through classes up to the parent class of Top.
350        while (myClass != JFrame.class) {
351            Field[] fields = myClass.getDeclaredFields();
352            for (Field field : fields) {
353                try {
354                    // Loop through the class hierarchy of each field.
355                    // If the class is assignable from AbstractAction, then
356                    // set it to null.
357                    Class superclass = field.getType();
358                    while (superclass != null && superclass != Object.class) {
359                        if (superclass.isAssignableFrom(AbstractAction.class)) {
360                            try {
361                                field.setAccessible(true);
362                                field.set(this, null);
363                            } catch (SecurityException ex) {
364                                if (!_printedSecurityExceptionMessage) {
365                                    _printedSecurityExceptionMessage = true;
366                                    System.out.println("Warning: Failed set "
367                                            + field
368                                            + " accessible while disposing. "
369                                            + "(applets and -sandbox always causes this)");
370                                }
371                            }
372                            break;
373                        }
374                        superclass = superclass.getSuperclass();
375                    }
376                } catch (IllegalAccessException ex) {
377                    throw new RuntimeException(
378                            "Failed to get or set field " + field, ex);
379                }
380            }
381            myClass = myClass.getSuperclass();
382        }
383
384        getContentPane().removeAll();
385        _disposed = true;
386
387        java.awt.image.BufferStrategy bufferStrategy = getBufferStrategy();
388        if (bufferStrategy != null) {
389            bufferStrategy.dispose();
390        }
391
392        // Sigh.  Under Mac OS X, we need to deal with the peers by hand.
393        // This code is left commented out because it is Mac-specific and
394        // accessing ComponentPeer produces a warning.
395
396        // See https://wiki.eecs.berkeley.edu/ptexternal/Main/Main/MemoryLeaks#CPlatformWindow
397        // See http://stackoverflow.com/questions/19781877/mac-os-java-7-jdialog-dispose-memory-leak
398
399        // java.awt.peer.ComponentPeer peer = getPeer();
400
401        super.dispose();
402
403        // if (peer != null) {
404        //     try {
405        //         Class<?> componentPeerClass = Class.forName("sun.lwawt.LWComponentPeer");
406        //         Field target = componentPeerClass.getDeclaredField("target");
407        //         target.setAccessible(true);
408        //         target.set(peer, null);
409
410        //         Field window = peer.getClass().getDeclaredField("platformWindow");
411        //         window.setAccessible(true);
412
413        //         Object platformWindow = window.get(peer);
414        //         target = platformWindow.getClass().getDeclaredField("target");
415        //         target.setAccessible(true);
416        //         target.set(platformWindow, null);
417
418        //     } catch (Throwable throwable) {
419        //         if (_debugClosing) {
420        //             throwable.printStackTrace();
421        //         }
422        //     }
423        // }
424    }
425
426    /** Exit the application after querying the user to save data.
427     *  Derived classes should override {@link #_exit()}.
428     *  @return False if the use cancelled a saving a modified buffer,
429     *  true otherwise.
430     */
431    public final boolean exit() {
432        // Under Mac OS X, Command-q invokes this method.
433        if (_debugClosing) {
434            System.out.println("Top.exit() : " + this.getName());
435        }
436        _exit();
437        if (_exitResult == _CANCELED) {
438            return false;
439        }
440        return true;
441    }
442
443    /** Return true if the window is set to be centered when pack() is called.
444     *  @return True if the window will be centered when pack is called.
445     *  @see #setCentering(boolean)
446     */
447    public boolean getCentering() {
448        return _centering;
449    }
450
451    /** Return the size of the contents of this window.
452     *  @return The size of the contents.
453     */
454    public Dimension getContentSize() {
455        return getContentPane().getSize();
456    }
457
458    /** If called before the first time pack() is called, this
459     *  method will prevent the appearance of a menu bar. This is
460     *  rarely desirable, but some subclasses of Top have to
461     *  contain panels that are not swing components. Such
462     *  components do not work with menus (the menu seems to
463     *  appear behind the component instead of in front of it).
464     *  Call this to prevent a menu bar.
465     */
466    public void hideMenuBar() {
467        _hideMenuBar = true;
468    }
469
470    /** Return true if this frame has been disposed.
471     *  It is possible for the dispose() method to be called directly.
472     *  This may conflict with another dispose call
473     *  in a window listener or somewhere else.  Before Top
474     *  calls super.dispose() (and thus triggering listeners) this
475     *  boolean is set to true so listeners can check and make sure
476     *  they aren't calling dispose for a second (or third) time.
477     *  @return true if this frame has been disposed.
478     */
479    public boolean isDisposed() {
480        return _disposed;
481    }
482
483    /** Return true if the menu of this window has been populated.
484     *  The menu is populated as a side effect of the first invocation to
485     *  the pack() method.
486     *  @return True if the menu bar has been populated.
487     */
488    public synchronized boolean isMenuPopulated() {
489        return _menuPopulated;
490    }
491
492    /** Return true if the data associated with this window has been
493     *  modified since it was first read or last saved.  This returns
494     *  the value set by calls to setModified(), or false if that method
495     *  has not been called.
496     *  @return True if the data has been modified.
497     */
498    public boolean isModified() {
499        return _modified;
500    }
501
502    /** Size this window to its preferred size and make it
503     *  displayable, and override the base class to populate the menu
504     *  bar if the menus have not already been populated.  If the
505     *  window size has not been set (by some derived class), then
506     *  this will center the window on the screen. This is
507     *  done here rather than in the constructor so that derived
508     *  classes are assured that their constructors have been fully
509     *  executed when _addMenus() is called. If this method is called
510     *  outside the AWT event thread, then its execution is deferred and
511     *  performed in that thread.
512     */
513    @Override
514    public void pack() {
515        Runnable doPack = new DoPackRunnable();
516
517        deferIfNecessary(doPack);
518    }
519
520    /** Report a message to the user by displaying it in a status bar,
521     *  if there is one. If this method is called outside the AWT event
522     *  thread, then its execution is deferred and performed in that thread.
523     *  @param message The message to report.
524     */
525    public void report(final String message) {
526        Runnable doReport = new StatusBarMessageRunnable(message);
527
528        deferIfNecessary(doReport);
529    }
530
531    /** Report a Throwable, which is usually an Exception but can also
532     *  be an Error. If this method is called outside the AWT event
533     *  thread, then its execution is deferred and performed in that thread.
534     *  This pops up a window with the option of examining the stack
535     *  trace, and reports the specified message in the status bar, if
536     *  there is one.
537     *  @param message The message.
538     *  @param throwable The Throwable to report.
539     */
540    public void report(final String message, final Throwable throwable) {
541        Runnable doReport = new StatusBarMessageReportRunnable(message,
542                throwable);
543
544        deferIfNecessary(doReport);
545    }
546
547    /** Report a Throwable, which is usually an Exception but can also
548     *        be an Error.  This displays a message in a dialog by
549     *  calling the two-argument version with an empty string as the
550     *  first argument.  If this method is called outside the AWT event
551     *  thread, then its execution is deferred and performed in that thread.
552     *  @param throwable The Throwable to report
553     *  @see #report(String, Throwable)
554     */
555    public void report(Throwable throwable) {
556        report("", throwable);
557    }
558
559    /** Set background color.  This overrides the base class to set the
560     *  background of the status bar. If this method is called outside
561     *  the AWT event thread, then its execution is deferred and
562     *  performed in that thread.
563     *  @param background The background color.
564     */
565    @Override
566    public void setBackground(final Color background) {
567        _statusBarBackground = background;
568        Runnable doSet = new SetBackgroundRunnable();
569
570        deferIfNecessary(doSet);
571    }
572
573    /** Specify whether or not to center the window on the screen when
574     *  packing it.  The default is true.
575     *  @param centering Set to false to disable centering.
576     *  @see #getCentering()
577     */
578    public void setCentering(boolean centering) {
579        _centering = centering;
580    }
581
582    /**
583     * Set the initial default directory.  If this method is not
584     * called, then the initial default directory will be the value of
585     * the user.dir Java property, which is typically the current
586     * working directory.  This method allows external configuration
587     * to determine the initial/default opening/saving directory to
588     * use for file dialogs.  (Used in Kepler)
589     * @param dir the initial directory to use for file dialogs
590     */
591    public static void setDirectory(File dir) {
592        _directory = dir;
593    }
594
595    /** Record whether the data associated with this window has been
596     *  modified since it was first read or last saved.  If you call
597     *  this with a true argument, then subsequent attempts to close
598     *  the window will trigger a dialog box to confirm the closing.
599     *  @param modified Indicator of whether the data has been modified.
600     */
601    public void setModified(boolean modified) {
602        _modified = modified;
603    }
604
605    /** Override the base class to deiconify
606     *  the window, if necessary. If this method is called
607     *  outside the AWT event thread, then its execution is deferred and
608     *  performed in that thread.
609     */
610    @Override
611    public void show() {
612        Runnable doShow = new ShowWindowRunnable();
613
614        deferIfNecessary(doShow);
615    }
616
617    /** Display the specified message in the status bar.
618     *  This message will be displayed for a maximum of MAXIMUM_STATUS_MESSAGE_TIME
619     *  (default 30 seconds).
620     *  If there is no status bar, print to standard out.
621     *  @param message The message.
622     */
623    @Override
624    public void status(final String message) {
625        if (_statusBar != null) {
626            if (_statusMessageTimer == null) {
627                // Second argument makes this a daemon thread, so it won't block exiting Vergil.
628                _statusMessageTimer = new Timer("Status message timer", true);
629            }
630            // The status bar update has to be performed in the Swing event thread.
631            // NOTE: If this is called from outside the Swing event thread, then
632            // the order in which messages are reported gets messed up.
633            // The only simple solutions seems to be to call this from the Swing event thread.
634            deferIfNecessary(new Runnable() {
635                @Override
636                public void run() {
637                    if (_lastStatusMessageClearingTask != null) {
638                        _lastStatusMessageClearingTask.cancel();
639                    }
640                    _statusBar.setMessage(message);
641
642                    // If the message is non-empty, schedule a clearing message.
643                    if (!message.trim().equals("")) {
644                        _lastStatusMessageClearingTask = new TimerTask() {
645                            @Override
646                            public void run() {
647                                status("");
648                                _lastStatusMessageClearingTask = null;
649                            }
650                        };
651                        _statusMessageTimer.schedule(
652                                _lastStatusMessageClearingTask,
653                                MAXIMUM_STATUS_MESSAGE_TIME);
654                    }
655                }
656            });
657        } else {
658            System.out.println(message);
659        }
660    }
661
662    /** Register with the global message handler to receive status messages.
663     *  @see MessageHandler#status(String)
664     *  @param event The window event.
665     */
666    @Override
667    public void windowGainedFocus(WindowEvent event) {
668        MessageHandler.setStatusHandler(this);
669    }
670
671    /** Unregister with the global message handler to receive status messages.
672     *  @see MessageHandler#status(String)
673     *  @param event The window event.
674     */
675    @Override
676    public void windowLostFocus(WindowEvent event) {
677        MessageHandler.setStatusHandler(null);
678    }
679
680    ///////////////////////////////////////////////////////////////////
681    ////                         public fields                     ////
682
683    /** Maximum amount of time that a status message is displayed in milliseconds. */
684    public static final long MAXIMUM_STATUS_MESSAGE_TIME = 30000;
685
686    ///////////////////////////////////////////////////////////////////
687    ////                         protected methods                 ////
688
689    /** Open a dialog with basic information about this window.
690     */
691    protected void _about() {
692        JOptionPane.showMessageDialog(this,
693                "Ptolemy II " + getClass().getName() + "\n"
694                        + "By: Claudius Ptolemaeus, ptolemy@eecs.berkeley.edu\n"
695                        + "For more information, see\n"
696                        + "http://ptolemy.eecs.berkeley.edu/ptolemyII\n\n"
697                        + "Copyright (c) 1997-2018, "
698                        + "The Regents of the University of California.",
699                "About Ptolemy II", JOptionPane.INFORMATION_MESSAGE);
700    }
701
702    /** Add menus to the menu bar.  In this base class, this does nothing.
703     *  In derived classes, however, it will add items with commands like
704     *  <pre>
705     *      JMenu newMenu = new JMenu("My Menu");
706     *      _menubar.add(newMenu);
707     *  </pre>
708     *  The reason for doing this in a protected method rather than
709     *  doing it directly in the constructor of the base class is subtle.
710     *  Unfortunately, at this time, Java provides no mechanism for
711     *  derived classes to insert menus at arbitrary points in the
712     *  menu bar.  Also, the menubar ignores the alignment property
713     *  of the JMenu.  By convention, however, we want the help menu to
714     *  be the rightmost menu.  Thus, we use a strategy pattern here,
715     *  and call a protected method that derived classes can use to
716     *  add menus.  Thus, this method is called before the Help menu
717     *  is added, and hence menus added in this method will appear to
718     *  the left of the Help menu.
719     */
720    protected void _addMenus() {
721    }
722
723    /** Clear the current contents.  This base class checks to see whether
724     *  the contents have been modified, and if so, then prompts the user
725     *  to save them.  Derived classes should override this method to
726     *  first call this parent class method, then clear the data,
727     *  unless the return value is false.  A return value of false
728     *  indicates that the user has canceled the action.
729     *  @return True if the current contents are either saved or discarded
730     *   with permission from the user.
731     */
732    protected boolean _clear() {
733        int result = _queryForSave();
734        return result == _SAVED || result == _DISCARDED;
735    }
736
737    /** Close the window.  Derived classes should override this to
738     *  release any resources or remove any listeners.  In this class,
739     *  if the data associated with this window has been modified, as
740     *  indicated by isModified(), then ask the user whether to save
741     *  the data before closing.
742     *  @return False if the user cancels on a save query.
743     */
744    protected boolean _close() {
745        if (_debugClosing) {
746            System.out.println("Top._close() : " + this.getName());
747        }
748
749        // NOTE: We use dispose() here rather than just hiding the
750        // window.  This ensures that derived classes can react to
751        // windowClosed events rather than overriding the
752        // windowClosing behavior given here.
753        if (isModified()) {
754            int result = _queryForSave();
755
756            if (result == _SAVED || result == _DISCARDED) {
757                dispose();
758                return true;
759            }
760
761            return false;
762        } else {
763            // Window is not modified, so just dispose.
764            dispose();
765            return true;
766        }
767    }
768
769    /** Create the items in the File menu. A null element in the array
770     *  represents a separator in the menu.
771     *
772     *  @return The items in the File menu.
773     */
774    @SuppressWarnings("deprecation") // Java 10, inanely, deprecated getMenuShortcutKeyMask and replaced it with getMenuShortcutKeyMaskEx, which has the same signature. This makes no sense and violates principles of OO design. They should have just fixed getMenuShortcutKeyMask.
775    protected JMenuItem[] _createFileMenuItems() {
776        JMenuItem[] fileMenuItems = new JMenuItem[13];
777
778        fileMenuItems[0] = new JMenuItem("Open File", KeyEvent.VK_O);
779        fileMenuItems[1] = new JMenuItem("Open URL", KeyEvent.VK_U);
780        // The following assumes _NEW_MENU_INDEX = 2.
781        fileMenuItems[_NEW_MENU_INDEX] = new JMenu("New");
782        fileMenuItems[3] = new JMenuItem("Save", KeyEvent.VK_S);
783        fileMenuItems[4] = new JMenuItem("Save As", KeyEvent.VK_A);
784        // The following assumes _IMPORT_MENU_INDEX = 5.
785        fileMenuItems[_IMPORT_MENU_INDEX] = new JMenu("Import");
786        // The following assumes _EXPORT_MENU_INDEX = 6.
787        fileMenuItems[_EXPORT_MENU_INDEX] = new JMenu("Export");
788        fileMenuItems[7] = new JMenuItem("Print", KeyEvent.VK_P);
789        fileMenuItems[8] = new JMenuItem("Close", KeyEvent.VK_C);
790
791        // Separators
792        fileMenuItems[9] = null;
793        fileMenuItems[11] = null;
794
795        // History submenu
796        JMenu history = new JMenu("Recent Files");
797        fileMenuItems[10] = history;
798
799        // Exit
800        fileMenuItems[12] = new JMenuItem("Exit", KeyEvent.VK_Q);
801
802        if (StringUtilities.inApplet()) {
803            JMenuItem[] appletFileMenuItems = new JMenuItem[8];
804            System.arraycopy(fileMenuItems, 0, appletFileMenuItems, 0,
805                    appletFileMenuItems.length);
806            appletFileMenuItems[7] = fileMenuItems[10];
807            fileMenuItems = appletFileMenuItems;
808            // If we are in an applet, disable certain menu items.
809            fileMenuItems[0].setEnabled(false);
810            fileMenuItems[2].setEnabled(false);
811            fileMenuItems[3].setEnabled(false);
812            fileMenuItems[4].setEnabled(false);
813            fileMenuItems[_IMPORT_MENU_INDEX].setEnabled(false);
814            fileMenuItems[_EXPORT_MENU_INDEX].setEnabled(false);
815            return fileMenuItems;
816        }
817
818        // Open button = ctrl-o.
819        fileMenuItems[0].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O,
820                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
821
822        // The mnemonic isn't set in the static
823        // initializer because JMenu doesn't have an
824        // appropriate constructor.
825        fileMenuItems[2].setMnemonic(KeyEvent.VK_N);
826        fileMenuItems[_EXPORT_MENU_INDEX].setMnemonic(KeyEvent.VK_E);
827
828        // New Import and Export buttons disabled by default.
829        fileMenuItems[2].setEnabled(false);
830        fileMenuItems[_IMPORT_MENU_INDEX].setEnabled(false);
831        fileMenuItems[_EXPORT_MENU_INDEX].setEnabled(false);
832
833        // Save button = ctrl-s.
834        fileMenuItems[3].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
835                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
836
837        // Print button = ctrl-p.
838        fileMenuItems[7].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P,
839                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
840
841        // Print button disabled by default, unless this class implements
842        // one of the JDK1.2 printing interfaces.
843        if (Top.this instanceof Printable || Top.this instanceof Pageable) {
844            fileMenuItems[7].setEnabled(true);
845        } else {
846            fileMenuItems[7].setEnabled(false);
847        }
848
849        // Close button = ctrl-w.
850        fileMenuItems[8].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W,
851                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
852
853        // Close button = ctrl-q.
854        fileMenuItems[12].setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q,
855                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
856        // Sadly, the above doesn't actually work. Command-Q is not caught.
857
858        return fileMenuItems;
859    }
860
861    /** Exit the application after querying the user to save data.
862     *  Derived classes should override this to do something more
863     *  reasonable, so that user data is not discarded.
864     */
865    protected void _exit() {
866        if (isModified()) {
867            int result = _queryForSave();
868            _exitResult = result;
869            if (result == _SAVED || result == _DISCARDED) {
870                StringUtilities.exit(0);
871            }
872        } else {
873            // Window is not modified, so just exit.
874            StringUtilities.exit(0);
875        }
876    }
877
878    /** Return the current directory.
879     * If {@link #setDirectory(File)} or
880     * {@link #_open()}, then the value of the "user.dir"
881     * property is returned.
882     * @return The current directory.
883     */
884    protected File _getCurrentDirectory() {
885        if (_directory != null) {
886            return _directory;
887        } else {
888            // The default on Windows is to open at user.home, which is
889            // typically not what we want.
890            // So we use the current directory instead.
891            // This will fail with a security exception in applets.
892            String currentWorkingDirectory = StringUtilities
893                    .getProperty("user.dir");
894            if (currentWorkingDirectory == null) {
895                return null;
896            } else {
897                return new File(currentWorkingDirectory);
898            }
899        }
900    }
901
902    /** Get the name of this object, which in this base class is
903     *  either the name of the file that has been associated with this
904     *  object, or the string "Unnamed" if none.
905     *  @return The name.
906     */
907    protected String _getName() {
908        if (_file == null) {
909            return "Unnamed";
910        }
911
912        return _file.getName();
913    }
914
915    /** Display the same information given by _about().
916     *  Derived classes should override this to give information
917     *  about the particular window and its role.
918     */
919    protected void _help() {
920        _about();
921    }
922
923    /** Open a file dialog to identify a file to be opened, and then call
924     *  _read() to open the file.  If {@link ptolemy.gui.PtGUIUtilities#useFileDialog()}
925     *  return true, then java.awt.FileDialog is used.
926     *  Otherwise, javax.swing.JFileChooser is used.
927     */
928    protected void _open() {
929        if (PtGUIUtilities.useFileDialog()) {
930            _openFileDialog();
931        } else {
932            _openJFileChooser();
933        }
934    }
935
936    /** Open a dialog to enter a URL, and then invoke
937     *  _read() to open the URL.
938     */
939    protected void _openURL() {
940        Query query = new Query();
941        query.setTextWidth(60);
942        query.addLine("url", "URL", _lastURL);
943
944        ComponentDialog dialog = new ComponentDialog(this, "Open URL", query);
945
946        if (dialog.buttonPressed().equals("OK")) {
947            _lastURL = query.getStringValue("url");
948
949            try {
950                URL url = new URL(_lastURL);
951                _read(url);
952            } catch (Exception ex) {
953                report("Error reading URL:\n" + _lastURL, ex);
954            }
955        }
956    }
957
958    /** Print the contents.  If this frame implements either the
959     *  Printable or Pageable then those interfaces are used to print
960     *  it.
961     */
962    protected void _print() {
963        // If you are using $PTII/bin/vergil, under bash, set this property:
964        // export JAVAFLAGS=-Dptolemy.ptII.print.platform=CrossPlatform
965        // and then run $PTII/bin/vergil
966        if (StringUtilities.getProperty("ptolemy.ptII.print.platform")
967                .equals("CrossPlatform")) {
968            _printCrossPlatform();
969        } else {
970            _printNative();
971        }
972    }
973
974    /** Print using the cross platform dialog.
975     *  Note that in java 1.6.0_05, the properties button is disabled,
976     *  so using _printNative() is preferred.
977     */
978    protected void _printCrossPlatform() {
979        // FIXME: Code duplication with PlotBox and PlotFrame.
980        // See PlotFrame for notes.
981
982        // Build a set of attributes
983        PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
984        PrinterJob job = PrinterJob.getPrinterJob();
985
986        _macCheck();
987
988        if (this instanceof Pageable) {
989            job.setPageable((Pageable) this);
990        } else if (this instanceof Printable) {
991            //PageFormat format = job.pageDialog(job.defaultPage());
992            //job.setPrintable((Printable) this, format);
993            job.setPrintable((Printable) this);
994        } else {
995            // Can't print it.
996            return;
997        }
998
999        if (job.printDialog(aset)) {
1000            try {
1001                job.print(aset);
1002            } catch (Exception ex) {
1003                MessageHandler.error("Cross Platform Printing Failed", ex);
1004            }
1005        }
1006    }
1007
1008    /** If a PDF printer is available print to it.
1009     *  @exception PrinterException If a printer with the string "PDF"
1010     * cannot be found or if the job cannot be set to the PDF print
1011     * service or if there is another problem printing.
1012     */
1013    protected void _printPDF() throws PrinterException {
1014        // Find something that will print to PDF
1015        boolean foundPDFPrinter = false;
1016
1017        PrintService pdfPrintService = null;
1018        PrintService printServices[] = PrinterJob.lookupPrintServices();
1019        for (PrintService printService : printServices) {
1020            if (printService.getName().indexOf("PDF") != -1) {
1021                foundPDFPrinter = true;
1022                pdfPrintService = printService;
1023            }
1024        }
1025
1026        if (pdfPrintService == null || foundPDFPrinter == false) {
1027            throw new PrinterException("Could not find a printer with the "
1028                    + "string \"PDF\" in its name.  Currently, the -printPDF "
1029                    + "facility requires a PDF printer such as the non-free "
1030                    + "full version of Adobe Acrobat.");
1031        }
1032
1033        PrinterJob job = PrinterJob.getPrinterJob();
1034        PageFormat pageFormat = job.defaultPage();
1035        job.setPrintService(pdfPrintService);
1036
1037        PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
1038
1039        _macCheck();
1040
1041        if (this instanceof Pageable) {
1042            // FIXME: what about the page format?
1043            job.setPageable((Pageable) this);
1044            job.validatePage(pageFormat);
1045        } else if (this instanceof Printable) {
1046            job.setPrintable((Printable) this, pageFormat);
1047        } else {
1048            System.out.println("Can't print a " + this
1049                    + ", it must be either Pageable or Printable");
1050            // Can't print it.
1051            return;
1052        }
1053        if (foundPDFPrinter) {
1054            // This gets ignored, but let's try it anyway
1055            Destination destination = new Destination(
1056                    new File("ptolemy.pdf").toURI());
1057            aset.add(destination);
1058
1059            // On the Mac, calling job.setJobName() will set the file name,
1060            // but not the directory.
1061            System.out.println(
1062                    "Top._printPDF(): Print Job information, much of which is ignored?\n"
1063                            + "JobName: " + job.getJobName() + "\nUserName: "
1064                            + job.getUserName());
1065            javax.print.attribute.Attribute[] attributes = aset.toArray();
1066            for (Attribute attribute : attributes) {
1067                System.out.println(attribute.getName() + " "
1068                        + attribute.getCategory() + " " + attribute);
1069            }
1070
1071            job.print(aset);
1072            System.out.println("Window printed from command line. "
1073                    + "Under MacOSX, look for "
1074                    + "~/Desktop/Java Printing.pdf");
1075        }
1076    }
1077
1078    /** Print using the native dialog.
1079     */
1080    protected void _printNative() {
1081        // FIXME: Code duplication with PlotBox and PlotFrame.
1082        // See PlotFrame for notes.
1083
1084        PrinterJob job = PrinterJob.getPrinterJob();
1085        PageFormat defaultFormat = job.defaultPage();
1086        PageFormat pageFormat = job.pageDialog(defaultFormat);
1087
1088        // If the print dialog is cancelled, we do not handle the job any more.
1089        // -- tfeng (12/12/2008)
1090        if (defaultFormat == pageFormat) {
1091            return;
1092        }
1093
1094        _macCheck();
1095
1096        if (this instanceof Pageable) {
1097            // FIXME: what about the page format?
1098            job.setPageable((Pageable) this);
1099            job.validatePage(pageFormat);
1100        } else if (this instanceof Printable) {
1101            job.setPrintable((Printable) this, pageFormat);
1102        } else {
1103            // Can't print it.
1104            return;
1105        }
1106
1107        if (job.printDialog()) {
1108            try {
1109                job.print();
1110            } catch (Exception ex) {
1111                MessageHandler.error("Native Printing Failed", ex);
1112            }
1113        }
1114    }
1115
1116    /** Open a dialog to prompt the user to save the data.
1117     *  Return false if the user clicks "cancel", and otherwise return true.
1118     *  If the user clicks "Save", this also saves the data.
1119     *  @return _SAVED if the file is saved, _DISCARDED if the modifications are
1120     *   discarded, _CANCELED if the operation is canceled by the user, and
1121     *   _FAILED if the user selects save and the save fails.
1122     */
1123    protected int _queryForSave() {
1124        Object[] options = { "Save", "Discard changes", "Cancel" };
1125
1126        String query = "Save changes to " + StringUtilities.split(_getName())
1127                + "?";
1128
1129        // Show the MODAL dialog
1130        int selected = JOptionPane.showOptionDialog(this, query,
1131                "Save Changes?", JOptionPane.YES_NO_CANCEL_OPTION,
1132                JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
1133
1134        if (selected == 0) {
1135            if (_save()) {
1136                return _SAVED;
1137            } else {
1138                return _FAILED;
1139            }
1140        }
1141
1142        if (selected == 1) {
1143            // To prevent the user from being asked again:
1144            setModified(false);
1145            return _DISCARDED;
1146        }
1147
1148        return _CANCELED;
1149    }
1150
1151    /** Read the specified URL.
1152     *  @param url The URL to read.
1153     *  @exception Exception If the URL cannot be read.
1154     */
1155    protected abstract void _read(URL url) throws Exception;
1156
1157    /** Save the model to the current file, if there is one, and otherwise
1158     *  invoke _saveAs().  This calls _writeFile().
1159     *  @return True if the save succeeds.
1160     */
1161    protected boolean _save() {
1162        if (_file != null) {
1163            try {
1164                _writeFile(_file);
1165                setModified(false);
1166                return true;
1167            } catch (IOException ex) {
1168                report("Error writing file", ex);
1169                return false;
1170            }
1171        } else {
1172            return _saveAs();
1173        }
1174    }
1175
1176    /** Query the user for a filename and save the model to that file.
1177     *  If {@link ptolemy.gui.PtGUIUtilities#useFileDialog()}
1178     *  return true, then java.awt.FileDialog is used.
1179     *  Otherwise, javax.swing.JFileChooser is used.
1180     *  @return True if the save succeeds.
1181     */
1182    protected boolean _saveAs() {
1183        if (PtGUIUtilities.useFileDialog()) {
1184            return _saveAsFileDialogImplementation();
1185        } else {
1186            return _saveAsJFileChooserImplementation();
1187        }
1188    }
1189
1190    /** Create and return a file dialog for the "Save As" command.
1191     *  @return A file dialog for save as.
1192     *  @deprecated Use {@link #_saveAsJFileChooserComponent()}
1193     */
1194    @Deprecated
1195    protected JFileChooser _saveAsFileDialog() {
1196        // Too bad this method was incorrectly named.  This method
1197        // returns a JFileChooser, why on earth would its name
1198        // have "FileDialog" in it?
1199        return _saveAsJFileChooserComponent();
1200    }
1201
1202    /** Create and return a FileDialog for the "Save As" command.
1203     *  If {@link ptolemy.gui.PtGUIUtilities#useFileDialog()} returns true,
1204     *  then {@link #_saveAs()} uses this method.  Otherwise,
1205     *  {@link #_saveAsJFileChooserComponent()} is used.
1206     *  @return A file dialog for save as.
1207     */
1208    protected FileDialog _saveAsFileDialogComponent() {
1209        // The name of this method ends in "Component" because
1210        // there already was a _saveAsFileDialog() method.
1211        FileDialog fileDialog = new FileDialog(this, "Save as...",
1212                FileDialog.SAVE);
1213
1214        if (_filenameFilter != null) {
1215            fileDialog.setFilenameFilter(_filenameFilter);
1216        }
1217
1218        fileDialog.setDirectory(_getCurrentDirectory().toString());
1219        return fileDialog;
1220    }
1221
1222    /** Create and return a JFileChooser for the "Save As" command.
1223     *  If {@link ptolemy.gui.PtGUIUtilities#useFileDialog()} returns false,
1224     *  then {@link #_saveAs()} uses this method.  Otherwise,
1225     *  {@link #_saveAsFileDialogComponent()} is used.
1226     *  @return A JFileChooser for save as.
1227     */
1228    protected JFileChooser _saveAsJFileChooserComponent() {
1229        JFileChooser fileDialog = new JFileChooser();
1230
1231        if (_fileFilter != null) {
1232            fileDialog.addChoosableFileFilter(_fileFilter);
1233        }
1234
1235        fileDialog.setDialogTitle("Save as...");
1236        fileDialog.setCurrentDirectory(_getCurrentDirectory());
1237        return fileDialog;
1238    }
1239
1240    /** Update the submenu with a history list
1241     * and add a listener to each line.
1242     * @param historyList The list of history items,
1243     * where each element is a String is the name of the
1244     * menu item.
1245     */
1246    protected void _populateHistory(List historyList) {
1247        //System.out.println("_populateHistory("+historyList.size()+")");
1248
1249        // Make sure we have a reference to the Recent Files menu
1250        if (_historyMenu == null) {
1251            Component[] components = _fileMenu.getMenuComponents();
1252            for (Component component : components) {
1253                if (component instanceof JMenu) {
1254                    JMenu menu = (JMenu) component;
1255                    String menuText = menu.getText();
1256                    if (menuText.equals("Recent Files")) {
1257                        _historyMenu = (JMenu) component;
1258                    }
1259                }
1260            }
1261            // If we found it, add to set of history menus
1262            if (_historyMenu != null) {
1263                _historyMenusAndListeners.put(_historyMenu,
1264                        _historyMenuListener);
1265            }
1266        }
1267
1268        //System.out.println("number of history menus: " + _historyMenus.size());
1269
1270        // Update the history menu in each Top
1271        for (Map.Entry<JMenu, HistoryMenuListener> entry : _historyMenusAndListeners
1272                .entrySet()) {
1273            //System.out.println("updating menu " + historyMenu);
1274
1275            JMenu historyMenu = entry.getKey();
1276
1277            /*int c =*/MemoryCleaner.removeActionListeners(historyMenu);
1278            //System.out.println("_historyMenu: "+c);
1279
1280            historyMenu.removeAll();
1281
1282            for (int i = 0; i < historyList.size(); i++) {
1283                String recentFileString = (String) historyList.get(i);
1284                JMenuItem item = new JMenuItem(recentFileString);
1285                item.addActionListener(entry.getValue());
1286                historyMenu.add(item);
1287            }
1288        }
1289    }
1290
1291    /** Add the name of the last file open or set the name to the
1292     * first position if already in the list.
1293     * @param file name of the file to add
1294     * @param delete If true, remove from the history list, otherwise
1295     * the file is added to the beginning.
1296     * @exception IOException If the history file cannot be created, written to,
1297     * or saved.
1298     */
1299    protected void _updateHistory(String file, boolean delete)
1300            throws IOException {
1301        List<String> historyList = _readHistory();
1302
1303        // Remove if already present (then added to first position)
1304        for (int i = 0; i < historyList.size(); i++) {
1305            if (historyList.get(i).equals(file)) {
1306                historyList.remove(i);
1307            }
1308        }
1309
1310        // Remove if depth > limit
1311        if (historyList.size() >= _historyDepth) {
1312            historyList.remove(historyList.size() - 1);
1313        }
1314
1315        // Add to fist position
1316        if (!delete) {
1317            historyList.add(0, file);
1318        }
1319
1320        // Serialize history
1321        _writeHistory(historyList);
1322
1323        // Update submenu
1324        _populateHistory(historyList);
1325    }
1326
1327    ///////////////////////////////////////////////////////////////////
1328    ////                         protected variables               ////
1329
1330    /** Write the model to the specified file.
1331     *  @param file The file to write to.
1332     *  @exception IOException If the write fails.
1333     */
1334    protected abstract void _writeFile(File file) throws IOException;
1335
1336    /** Indicator that a close operation is canceled. */
1337    protected static final int _CANCELED = 2;
1338
1339    /** Indicator that a file is discarded. */
1340    protected static final int _DISCARDED = 1;
1341
1342    /** Indicator that a file save failed. */
1343    protected static final int _FAILED = 3;
1344
1345    /** Indicator that a file is saved. */
1346    protected static final int _SAVED = 0;
1347
1348    /** The most recent directory used in a file dialog. */
1349    protected static File _directory = null;
1350
1351    /** The return value of the _exit() menu.
1352     *  We use a separate variable here for backward compatibility.
1353     *  The values of this variable are the values returned by _queryForSave.
1354     */
1355    protected int _exitResult = _CANCELED;
1356
1357    /** The FileFilter that determines what files are displayed by
1358     *  the Open dialog and the Save As dialog
1359     *  The initial default is null, which causes no FileFilter to be
1360     *  applied, which results in all files being displayed.
1361     */
1362    protected FileFilter _fileFilter = null;
1363
1364    /** The FileFilter used by the java.awt.FileDialog that determines
1365     *  what files are displayed by the Open dialog and the Save As
1366     *  dialog The initial default is null, which causes no FileFilter
1367     *  to be applied, which results in all files being displayed.
1368     *
1369     *  <p>Note that this class can use either java.awt.FileDialog
1370     *  or javax.swing.JFileChooser, so classes should set
1371     *  both _fileFilter and _filenameFilter. Note that it
1372     *  possible to define an inner class that extends
1373     *  javax.swing.filechooser.FileFilter and implements
1374     *  java.io.FilenameFilter and that has two separate accept()
1375     *  methods.  This inner class can then be set as the
1376     *  value for both _fileFilter and _filenameFilter:
1377     *  <pre>
1378     *  ...
1379     *      MyFileFilter myFileFilter = new MyFileFilter();
1380     *      _fileFilter = myFileFilter;
1381     *      _filenameFilter = myFileFilter;
1382     *  ...
1383     *  static MyFileFilter extends FileFilter implements FilenameFilter {
1384     *      public boolean accept(File file) {
1385     *         // For FileFilter
1386     *         return true;
1387     *      }
1388     *
1389     *      public boolean accept(File director, String name) {
1390     *         // For FilenameFilter
1391     *         return true;
1392     *      }
1393     *
1394     *      public String getDescription() {
1395     *         // For FileFilter
1396     *         return "My filter description.";
1397     *      }
1398     *  }
1399     *  </pre>
1400     */
1401    protected FilenameFilter _filenameFilter = null;
1402
1403    /** File menu for this frame. */
1404    protected JMenu _fileMenu = new JMenu("File");
1405
1406    /** Items in the file menu. A null element represents a separator. */
1407    protected JMenuItem[] _fileMenuItems = _createFileMenuItems();
1408
1409    /** Index in the _fileMenuItems array of the New item. */
1410    protected static int _NEW_MENU_INDEX = 2;
1411
1412    /** Index in the _fileMenuItems array of the Import item. */
1413    protected static int _IMPORT_MENU_INDEX = 5;
1414
1415    /** Index in the _fileMenuItems array of the Export item. */
1416    protected static int _EXPORT_MENU_INDEX = 6;
1417
1418    /** Help menu for this frame. */
1419    protected JMenu _helpMenu = new JMenu("Help");
1420
1421    /** Help menu items. */
1422    protected JMenuItem[] _helpMenuItems = {
1423            new JMenuItem("About", KeyEvent.VK_A),
1424            new JMenuItem("Help", KeyEvent.VK_H), };
1425
1426    /** Menubar for this frame. */
1427    protected JMenuBar _menubar = new JMenuBar();
1428
1429    /** The status bar. */
1430    protected StatusBar _statusBar = null;
1431
1432    /** Listener for file menu commands. */
1433    class FileMenuListener implements ActionListener {
1434        @Override
1435        public void actionPerformed(ActionEvent e) {
1436            // Make this the default context for modal messages.
1437            UndeferredGraphicalMessageHandler.setContext(Top.this);
1438
1439            JMenuItem target = (JMenuItem) e.getSource();
1440            String actionCommand = target.getActionCommand();
1441
1442            try {
1443                if (actionCommand.equals("Open File")) {
1444                    _open();
1445                } else if (actionCommand.equals("Open URL")) {
1446                    _openURL();
1447                } else if (actionCommand.equals("Save")) {
1448                    _save();
1449                } else if (actionCommand.equals("Save As")) {
1450                    _saveAs();
1451                } else if (actionCommand.equals("Print")) {
1452                    _print();
1453                } else if (actionCommand.equals("Close")) {
1454                    if (!isDisposed()) {
1455                        Window thisWindow = Top.this;
1456                        WindowEvent wev = new WindowEvent(thisWindow,
1457                                WindowEvent.WINDOW_CLOSING);
1458                        Toolkit toolkit = Toolkit.getDefaultToolkit();
1459                        EventQueue queue = toolkit.getSystemEventQueue();
1460                        queue.postEvent(wev);
1461                    }
1462                } else if (actionCommand.equals("Exit")) {
1463                    _exit();
1464                }
1465            } catch (Throwable throwable) {
1466                // If we do not catch exceptions here, then they
1467                // disappear to stdout, which is bad if we launched
1468                // where there is no stdout visible.
1469                MessageHandler.error("File Menu Exception:", throwable);
1470            }
1471
1472            // NOTE: The following should not be needed, but jdk1.3beta
1473            // appears to have a bug in swing where repainting doesn't
1474            // properly occur.
1475            repaint();
1476        }
1477    }
1478
1479    /** Listener for help menu commands. */
1480    class HelpMenuListener implements ActionListener {
1481        @Override
1482        public void actionPerformed(ActionEvent e) {
1483            // Make this the default context for modal messages.
1484            UndeferredGraphicalMessageHandler.setContext(Top.this);
1485
1486            JMenuItem target = (JMenuItem) e.getSource();
1487            String actionCommand = target.getActionCommand();
1488
1489            try {
1490                if (actionCommand.equals("About")) {
1491                    _about();
1492                } else if (actionCommand.equals("Help")) {
1493                    _help();
1494                }
1495            } catch (Throwable throwable) {
1496                // If we do not catch exceptions here, then they
1497                // disappear to stdout, which is bad if we launched
1498                // where there is no stdout visible.
1499                MessageHandler.error("Help Menu Exception:", throwable);
1500            }
1501
1502            // NOTE: The following should not be needed, but there jdk1.3beta
1503            // appears to have a bug in swing where repainting doesn't
1504            // properly occur.
1505            repaint();
1506        }
1507    }
1508
1509    ///////////////////////////////////////////////////////////////////
1510    ////                         private methods                   ////
1511
1512    // Execute all actions pending on the deferred action list.
1513    // The list is cleared and the _actionsDeferred variable is set
1514    // to false, even if one of the deferred actions fails.
1515    // This method should only be invoked in the event dispatch thread.
1516    // It is synchronized on the _deferredActions list, so the integrity
1517    // of that list is ensured, since modifications to that list occur
1518    // only in other places that are also synchronized on the list.
1519    private static void _executeDeferredActions() {
1520        // Copy the list so that we do not hold the synchronization lock
1521        // while executing the actions.
1522        LinkedList actionsCopy = null;
1523        synchronized (_deferredActions) {
1524            actionsCopy = new LinkedList(_deferredActions);
1525            _actionsDeferred = false;
1526            _deferredActions.clear();
1527        }
1528        Iterator actions = actionsCopy.iterator();
1529
1530        // Collect and report exceptions.
1531        LinkedList<Throwable> exceptions = null;
1532        while (actions.hasNext()) {
1533            Runnable action = (Runnable) actions.next();
1534            try {
1535                action.run();
1536            } catch (Throwable ex) {
1537                if (exceptions == null) {
1538                    exceptions = new LinkedList<Throwable>();
1539                }
1540                exceptions.add(ex);
1541            }
1542        }
1543        if (exceptions != null) {
1544            StringBuffer message = new StringBuffer(
1545                    "Exceptions occurred in deferred actions:\n");
1546            for (Throwable exception : exceptions) {
1547                message.append(exception);
1548                message.append("\n");
1549            }
1550            MessageHandler.error(message.toString(), exceptions.get(0));
1551        }
1552    }
1553
1554    /** Return the value of the history file name.
1555     *  @return The value of the history file name, which is usually in
1556     *  the Ptolemy II preferences directory.  The value returned is usually.
1557     *  "~/.ptolemyII/history.txt".
1558     *  @exception IOException If thrown while reading the preferences directory.
1559     */
1560    private String _getHistoryFileName() throws IOException {
1561        return StringUtilities.preferencesDirectory() + "history.txt";
1562    }
1563
1564    /** Open a file dialog using the java.awt.Dialog class to identify
1565     *  a file to be opened, and then call _read() to open the file.
1566     *  Under Mac OS X, this method is preferred over _openJFileChooser()
1567     *  because this method uses java.awt.Dialog, whereas _openJFileChooser()
1568     *  uses javax.swing.JFileChooser.
1569     */
1570    private void _openFileDialog() {
1571        FileDialog fileDialog = new FileDialog(this, "Select a model file",
1572                FileDialog.LOAD);
1573
1574        if (_filenameFilter != null) {
1575            fileDialog.setFilenameFilter(_filenameFilter);
1576        }
1577        boolean fail = false;
1578        if (_directory != null) {
1579            try {
1580                fileDialog.setDirectory(_directory.getCanonicalPath());
1581            } catch (IOException ex) {
1582                fail = true;
1583            }
1584        }
1585        if (fail || _directory == null) {
1586            // The default on Windows is to open at user.home, which is
1587            // typically an absurd directory inside the O/S installation.
1588            // So we use the current directory instead.
1589            // This will throw a security exception in an applet.
1590            // FIXME: we should support users under applets opening files
1591            // on the server.
1592            String currentWorkingDirectory = StringUtilities
1593                    .getProperty("user.dir");
1594
1595            if (currentWorkingDirectory != null) {
1596                fileDialog.setDirectory(currentWorkingDirectory);
1597            }
1598        }
1599
1600        fileDialog.show();
1601
1602        if (fileDialog.getDirectory() == null) {
1603            _directory = new File("");
1604        } else {
1605            _directory = new File(fileDialog.getDirectory());
1606        }
1607
1608        try {
1609            // NOTE: It would be nice if it were possible to enter
1610            // a URL in the file chooser, but Java's file chooser does
1611            // not permit this, regrettably.  So we have a separate
1612            // menu item for this.
1613            File file = new File(_directory, fileDialog.getFile());
1614            // Report on the time it takes to open the model.
1615            long startTime = System.currentTimeMillis();
1616            _read(file.toURI().toURL());
1617            long endTime = System.currentTimeMillis();
1618            if (endTime > startTime + 10000) {
1619                // Only print the time if it is more than 10
1620                // seconds See also PtolemyEffigy.  Perhaps
1621                // this code should be in PtolemyEffigy, but
1622                // if it is here, we get the time it takes to
1623                // read any file, not just a Ptolemy model.
1624                System.out.println("Opened " + file + " in "
1625                        + (System.currentTimeMillis() - startTime) + " ms.");
1626            }
1627            // Only add file if no exception
1628            _updateHistory(file.getAbsolutePath(), false);
1629
1630        } catch (Error error) {
1631            // Be sure to catch Error here so that if we throw an
1632            // Error, then we will report it to with a window.
1633            try {
1634                throw new RuntimeException(error);
1635            } catch (Exception ex2) {
1636                report("Error while reading input:", ex2);
1637            }
1638        } catch (Exception ex) {
1639            // NOTE: The XML parser can only throw an
1640            // XmlException.  It signals that it is a user
1641            // cancellation with the special string pattern
1642            // "*** Canceled." in the message.
1643
1644            if (ex.getMessage() != null
1645                    && !ex.getMessage().startsWith("*** Canceled.")) {
1646                // No need to report a CancelException, since
1647                // it results from the user clicking a
1648                // "Cancel" button.
1649                report("Error reading input", ex);
1650            }
1651        }
1652    }
1653
1654    /** Open a file dialog to identify a file to be opened, and then call
1655     *  _read() to open the file.
1656     */
1657    private void _openJFileChooser() {
1658        // Swap backgrounds and avoid white boxes in "common places" dialog
1659        JFileChooserBugFix jFileChooserBugFix = new JFileChooserBugFix();
1660        Color background = null;
1661        try {
1662            background = jFileChooserBugFix.saveBackground();
1663
1664            JFileChooser fileDialog = new JFileChooser();
1665
1666            // To disable the Windows places bar on the left, uncomment the
1667            // line below.
1668            // fileDialog.putClientProperty("FileChooser.useShellFolder", Boolean.FALSE);
1669            if (_fileFilter != null) {
1670                fileDialog.addChoosableFileFilter(_fileFilter);
1671            }
1672
1673            fileDialog.setDialogTitle("Select a model file.");
1674
1675            if (_directory != null) {
1676                fileDialog.setCurrentDirectory(_directory);
1677            } else {
1678                // The default on Windows is to open at user.home, which is
1679                // typically an absurd directory inside the O/S installation.
1680                // So we use the current directory instead.
1681                // This will throw a security exception in an applet.
1682                // FIXME: we should support users under applets opening files
1683                // on the server.
1684                String currentWorkingDirectory = StringUtilities
1685                        .getProperty("user.dir");
1686
1687                if (currentWorkingDirectory != null) {
1688                    fileDialog.setCurrentDirectory(
1689                            new File(currentWorkingDirectory));
1690                }
1691            }
1692
1693            if (fileDialog
1694                    .showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
1695                _directory = fileDialog.getCurrentDirectory();
1696
1697                try {
1698                    // NOTE: It would be nice if it were possible to enter
1699                    // a URL in the file chooser, but Java's file chooser does
1700                    // not permit this, regrettably.  So we have a separate
1701                    // menu item for this.
1702                    File file = fileDialog.getSelectedFile().getCanonicalFile();
1703                    // Report on the time it takes to open the model.
1704                    long startTime = System.currentTimeMillis();
1705                    _read(file.toURI().toURL());
1706                    long endTime = System.currentTimeMillis();
1707                    if (endTime > startTime + 10000) {
1708                        // Only print the time if it is more than 10
1709                        // seconds See also PtolemyEffigy.  Perhaps
1710                        // this code should be in PtolemyEffigy, but
1711                        // if it is here, we get the time it takes to
1712                        // read any file, not just a Ptolemy model.
1713                        System.out.println("Opened " + file + " in "
1714                                + (System.currentTimeMillis() - startTime)
1715                                + " ms.");
1716                    }
1717                    // Only add file if no exception
1718                    _updateHistory(file.getAbsolutePath(), false);
1719
1720                } catch (Error error) {
1721                    // Be sure to catch Error here so that if we throw an
1722                    // Error, then we will report it to with a window.
1723                    try {
1724                        throw new RuntimeException(error);
1725                    } catch (Exception ex2) {
1726                        report("Error while reading input:", ex2);
1727                    }
1728                } catch (Exception ex) {
1729                    // NOTE: The XML parser can only throw an
1730                    // XmlException.  It signals that it is a user
1731                    // cancellation with the special string pattern
1732                    // "*** Canceled." in the message.
1733
1734                    if (ex.getMessage() != null
1735                            && !ex.getMessage().startsWith("*** Canceled.")) {
1736                        // No need to report a CancelException, since
1737                        // it results from the user clicking a
1738                        // "Cancel" button.
1739                        report("Error reading input", ex);
1740                    }
1741                }
1742            }
1743        } finally {
1744            jFileChooserBugFix.restoreBackground(background);
1745        }
1746    }
1747
1748    /** If we are running under Mac OS X and Java 1.5, the print a
1749     * message about printing problems.
1750     */
1751    private static void _macCheck() {
1752        if (PtGUIUtilities.macOSLookAndFeel()
1753                && System.getProperty("java.version").startsWith("1.5")) {
1754            System.out.println(
1755                    "Warning, under Mac OS X with Java 1.5, printing might "
1756                            + "not work.  Try recompiling with Java 1.6 or setting a property:\n"
1757                            + "export JAVAFLAGS=-Dptolemy.ptII.print.platform=CrossPlatform\n"
1758                            + "and restarting vergil: $PTII/bin/vergil");
1759        }
1760    }
1761
1762    /** Initialize the menus and key bindings for Mac OS X. */
1763    private void _macInitializer() {
1764        // FIXME: This code causes a memory leak because
1765        // MacOSXAdapter and JPopupMenu return retain references to
1766        // ActorGraphFrame.  Also, MacOSXAdapter uses deprecated
1767        // methods via reflection.
1768        try {
1769            // To set the about name, set the
1770            // com.apple.mrj.application.apple.menu.about.name
1771            // property in the main thread, not the event thread.  See
1772            // VergilApplication.java
1773            MacOSXAdapter.setAboutMethod(this,
1774                    getClass().getMethod("about", (Class[]) null));
1775            MacOSXAdapter.setQuitMethod(this,
1776                    getClass().getMethod("exit", (Class[]) null));
1777        } catch (NoSuchMethodException ex) {
1778            report("Mac OS X specific initializations failed.", ex);
1779        }
1780    }
1781
1782    /** Get the history from the file that contains names
1783     * Always return a list, that can be empty
1784     * @return list of file history
1785     */
1786    private List<String> _readHistory() throws IOException {
1787        ArrayList<String> historyList = new ArrayList<String>();
1788        String historyFileName = _getHistoryFileName();
1789        if (!new File(historyFileName).exists()) {
1790            // No history file, so just return
1791            return historyList;
1792        }
1793        FileReader fileReader = null;
1794        BufferedReader bufferedReader = null;
1795        try {
1796            fileReader = new FileReader(historyFileName);
1797            bufferedReader = new BufferedReader(fileReader);
1798            String line;
1799            while ((line = bufferedReader.readLine()) != null) {
1800                historyList.add(line);
1801            }
1802        } finally {
1803            if (bufferedReader != null) {
1804                bufferedReader.close();
1805            }
1806        }
1807
1808        return historyList;
1809    }
1810
1811    /** Query the user for a filename and save the model to that file.
1812     *  @return True if the save succeeds.
1813     */
1814    private boolean _saveAsFileDialogImplementation() {
1815        // This method name ends with "Implementation" because there
1816        // already was a _saveAsFileDialog() method
1817
1818        // Use the strategy pattern here to create the actual
1819        // dialog so that subclasses can customize this dialog.
1820        FileDialog fileDialog = _saveAsFileDialogComponent();
1821
1822        if (fileDialog == null) {
1823            // Action was canceled.
1824            return false;
1825        }
1826
1827        fileDialog.show();
1828
1829        _directory = new File(fileDialog.getDirectory());
1830
1831        _file = new File(_directory, fileDialog.getFile());
1832
1833        // If the file exists, then FileDialog will prompt the user
1834        // so we don't want to prompt them again.
1835        // See "saving xml to existing file asks twice on mac"
1836        // http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5760
1837        //         if (_file.exists()) {
1838        //             // Ask for confirmation before overwriting a file.
1839        //             String query = "Overwrite " + _file.getName() + "?";
1840        //
1841        //             // Show a MODAL dialog
1842        //             int selected = JOptionPane.showOptionDialog(this, query,
1843        //                     "Save Changes?", JOptionPane.YES_NO_OPTION,
1844        //                     JOptionPane.QUESTION_MESSAGE, null, null, null);
1845        //
1846        //             if (selected == 1) {
1847        //                 return false;
1848        //             }
1849        //         }
1850
1851        // Truncate the name so that dialogs under Web Start on the Mac
1852        // work better.
1853        setTitle(StringUtilities.abbreviate(_getName()));
1854        return _save();
1855    }
1856
1857    /** Query the user for a filename and save the model to that file.
1858     *  @return True if the save succeeds.
1859     */
1860    private boolean _saveAsJFileChooserImplementation() {
1861        // Swap backgrounds and avoid white boxes in "common places" dialog
1862        JFileChooserBugFix jFileChooserBugFix = new JFileChooserBugFix();
1863        Color background = null;
1864        try {
1865            background = jFileChooserBugFix.saveBackground();
1866
1867            // Use the strategy pattern here to create the actual
1868            // dialog so that subclasses can customize this dialog.
1869            JFileChooser fileDialog = _saveAsFileDialog();
1870
1871            // Under Java 1.6 and Mac OS X, showSaveDialog() ignores the filter.
1872            if (fileDialog
1873                    .showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
1874                _file = fileDialog.getSelectedFile();
1875
1876                if (_file.exists()) {
1877                    // Ask for confirmation before overwriting a file.
1878                    String query = "Overwrite " + _file.getName() + "?";
1879
1880                    // Show a MODAL dialog
1881                    int selected = JOptionPane.showOptionDialog(this, query,
1882                            "Save Changes?", JOptionPane.YES_NO_OPTION,
1883                            JOptionPane.QUESTION_MESSAGE, null, null, null);
1884
1885                    if (selected == 1) {
1886                        return false;
1887                    }
1888                }
1889
1890                // Truncate the name so that dialogs under Web Start on the Mac
1891                // work better.
1892                setTitle(StringUtilities.abbreviate(_getName()));
1893                _directory = fileDialog.getCurrentDirectory();
1894                return _save();
1895            }
1896
1897            // Action was canceled.
1898            return false;
1899        } finally {
1900            jFileChooserBugFix.restoreBackground(background);
1901        }
1902    }
1903
1904    /** Write history to the file defined by _getHistoryFileName(). */
1905    private void _writeHistory(List<String> historyList) throws IOException {
1906        FileWriter fileWriter = null;
1907        try {
1908            fileWriter = new FileWriter(_getHistoryFileName());
1909            for (String line : historyList) {
1910                fileWriter.write(line + "\n");
1911            }
1912        } finally {
1913            if (fileWriter != null) {
1914                fileWriter.close();
1915            }
1916        }
1917    }
1918
1919    ///////////////////////////////////////////////////////////////////
1920    ////                         protected variables               ////
1921
1922    /** Set to true to print closing sequence information to standard
1923     * out.
1924     */
1925    protected boolean _debugClosing = false;
1926
1927    ///////////////////////////////////////////////////////////////////
1928    ////                         inner classes                     ////
1929
1930    /** A runnable for showing the window. */
1931    class ShowWindowRunnable implements Runnable {
1932        @Override
1933        public void run() {
1934            // NOTE: We used to call pack() here, but this would
1935            // override any manual changes in sizing that had been
1936            // made.
1937            setState(Frame.NORMAL);
1938            // FIXME: show() is deprecated, but calling setVisible()
1939            // here results in a loop.
1940            //Top.super.setVisible(true);
1941            Top.super.show();
1942
1943        }
1944    }
1945
1946    /** A runnable for setting the status bar message for a report. */
1947    class StatusBarMessageReportRunnable implements Runnable {
1948        public StatusBarMessageReportRunnable(String message,
1949                Throwable throwable) {
1950            _message = message;
1951            _throwable = throwable;
1952        }
1953
1954        @Override
1955        public void run() {
1956            if (_statusBar != null) {
1957                _statusBar
1958                        .setMessage(MessageHandler.shortDescription(_throwable)
1959                                + ". " + _message);
1960            }
1961
1962            MessageHandler.error(_message, _throwable);
1963        }
1964
1965        private String _message;
1966        private Throwable _throwable;
1967    }
1968
1969    /** A runnable for setting the status bar message. */
1970    class StatusBarMessageRunnable implements Runnable {
1971        public StatusBarMessageRunnable(String message) {
1972            _message = message;
1973        }
1974
1975        @Override
1976        public void run() {
1977            if (_statusBar != null) {
1978                _statusBar.setMessage(_message);
1979            }
1980        }
1981
1982        private String _message;
1983    }
1984
1985    /** A runnable for packing the Window. */
1986    class DoPackRunnable implements Runnable {
1987        @Override
1988        public void run() {
1989            // NOTE: This always runs in the swing thread,
1990            // so there is no need to synchronize.
1991            if (!_menuPopulated) {
1992                // Set up the menus.
1993                _fileMenu.setMnemonic(KeyEvent.VK_F);
1994                _helpMenu.setMnemonic(KeyEvent.VK_H);
1995
1996                // Construct the File menu by adding action commands
1997                // and action listeners.
1998
1999                // Set the action command and listener for each menu item.
2000                for (JMenuItem _fileMenuItem : _fileMenuItems) {
2001                    if (_fileMenuItem == null) {
2002                        _fileMenu.addSeparator();
2003                    } else {
2004                        _fileMenuItem.setActionCommand(_fileMenuItem.getText());
2005                        _fileMenuItem.addActionListener(_fileMenuListener);
2006                        _fileMenu.add(_fileMenuItem);
2007                    }
2008                }
2009
2010                _menubar.add(_fileMenu);
2011
2012                // History fill
2013                try {
2014                    _populateHistory(_readHistory());
2015                } catch (IOException ex) {
2016                    // Ignore
2017                } catch (SecurityException ex) {
2018                    // Ignore
2019                }
2020
2021                // Construct the Help menu by adding action commands
2022                // and action listeners.
2023
2024                // Set the action command and listener for each menu item.
2025                for (JMenuItem _helpMenuItem : _helpMenuItems) {
2026                    _helpMenuItem.setActionCommand(_helpMenuItem.getText());
2027                    _helpMenuItem.addActionListener(_helpMenuListener);
2028                    _helpMenu.add(_helpMenuItem);
2029                }
2030
2031                // Unfortunately, at this time, Java provides no
2032                // mechanism for derived classes to insert menus
2033                // at arbitrary points in the menu bar.  Also, the
2034                // menubar ignores the alignment property of the
2035                // JMenu.  By convention, however, we want the
2036                // help menu to be the rightmost menu.  Thus, we
2037                // use a strategy pattern here, and call a
2038                // protected method that derived classes can use
2039                // to add menus.
2040                _addMenus();
2041
2042                _menubar.add(_helpMenu);
2043
2044                if (!_hideMenuBar) {
2045                    setJMenuBar(_menubar);
2046                }
2047
2048                // Add the status bar, if there is one.
2049                if (_statusBar != null) {
2050                    getContentPane().add(_statusBar, BorderLayout.SOUTH);
2051                }
2052            }
2053
2054            Top.super.pack();
2055
2056            if (_centering) {
2057                centerOnScreen();
2058            }
2059            _menuPopulated = true;
2060        }
2061    }
2062
2063    /** A runnable for executing deferred actions. */
2064    static class DeferredActionsRunnable implements Runnable {
2065        @Override
2066        public void run() {
2067            _executeDeferredActions();
2068        }
2069    }
2070
2071    /** A runnable for closing the window. */
2072    class CloseWindowRunnable implements Runnable {
2073        @Override
2074        public void run() {
2075            _close();
2076        }
2077    }
2078
2079    /** A runnable for centering the window on the screen. */
2080    class CenterOnScreenRunnable implements Runnable {
2081        @Override
2082        public void run() {
2083            Toolkit tk = Toolkit.getDefaultToolkit();
2084            setLocation((tk.getScreenSize().width - getSize().width) / 2,
2085                    (tk.getScreenSize().height - getSize().height) / 2);
2086
2087            // Make this the default context for modal messages.
2088            UndeferredGraphicalMessageHandler.setContext(Top.this);
2089        }
2090    }
2091
2092    /** A runnable for setting the background color of the status bar. */
2093    class SetBackgroundRunnable implements Runnable {
2094        @Override
2095        public void run() {
2096            Top.super.setBackground(_statusBarBackground);
2097
2098            // This seems to be called in a base class
2099            // constructor, before this variable has been
2100            // set. Hence the test against null.
2101            if (_statusBar != null) {
2102                _statusBar.setBackground(_statusBarBackground);
2103            }
2104        }
2105    }
2106
2107    /** Listener for windowClosing action. */
2108    class CloseWindowAdapter extends WindowAdapter {
2109        @Override
2110        public void windowClosing(WindowEvent e) {
2111            if (_debugClosing) {
2112                System.out.println("Top$CloseWindowAdapter.windowClosing() : "
2113                        + Top.this.getName());
2114            }
2115
2116            Window window = e.getWindow();
2117            if (window instanceof Top) {
2118                Top top = (Top) window;
2119                if (!top.isDisposed()) {
2120                    _close();
2121                }
2122            }
2123        }
2124    }
2125
2126    /** Listener for history menu commands. */
2127    class HistoryMenuListener implements ActionListener {
2128        @Override
2129        public void actionPerformed(ActionEvent event) {
2130            // Make this the default context for modal messages.
2131            UndeferredGraphicalMessageHandler.setContext(Top.this);
2132
2133            JMenuItem target = (JMenuItem) event.getSource();
2134            String path = target.getActionCommand();
2135
2136            File file = new File(path);
2137
2138            // See if the file is still there.
2139            if (!file.exists()) {
2140                MessageHandler.error("File no longer exists.");
2141                try {
2142                    _updateHistory(path, true);
2143                } catch (IOException ex2) {
2144                    // Ignore
2145                }
2146                return;
2147            }
2148
2149            // Try to read the file.
2150            try {
2151                _read(file.toURI().toURL());
2152            } catch (Exception e) {
2153                MessageHandler.error("Error reading " + file.getAbsolutePath(),
2154                        e);
2155                try {
2156                    _updateHistory(path, true);
2157                } catch (IOException ex2) {
2158                    // Ignore
2159                }
2160                return;
2161            }
2162
2163            // Try to move the file to the top of the history menu.
2164            try {
2165                _updateHistory(path, false);
2166            } catch (IOException e) {
2167                MessageHandler.error("Error updating history file.", e);
2168                return;
2169            }
2170
2171            setDirectory(file.getParentFile());
2172        }
2173    }
2174
2175    ///////////////////////////////////////////////////////////////////
2176    ////                         private variables                 ////
2177
2178    /** An ActionListener for the menu items in the file menu. */
2179    private FileMenuListener _fileMenuListener;
2180
2181    /** An ActionListener for the menu items in the help menu. */
2182    private HelpMenuListener _helpMenuListener;
2183
2184    /** An ActionListener for the menu items in the history menu. */
2185    private HistoryMenuListener _historyMenuListener;
2186
2187    /** A reference to the history menu. */
2188    private JMenu _historyMenu;
2189
2190    /** A mapping of history menu to history menu listener for all open Top
2191     *  windows.
2192     */
2193    private static Map<JMenu, HistoryMenuListener> _historyMenusAndListeners = Collections
2194            .synchronizedMap(new WeakHashMap<JMenu, HistoryMenuListener>());
2195
2196    /** The background color of the status bar */
2197    private Color _statusBarBackground;
2198
2199    /** Indicator of whether actions are deferred. */
2200    private static boolean _actionsDeferred = false;
2201
2202    // A flag indicating whether or not to center the window.
2203    private boolean _centering = true;
2204
2205    /** List of deferred actions. */
2206    private static List _deferredActions = new LinkedList();
2207
2208    /** True if this frame has been disposed. */
2209    private boolean _disposed = false;
2210
2211    /** The input file. */
2212    private File _file = null;
2213
2214    /** Flag to hide the menu bar. */
2215    private boolean _hideMenuBar = false;
2216
2217    /** History depth. */
2218    private int _historyDepth = 10;
2219
2220    /** The most recently entered URL in Open URL. */
2221    private String _lastURL = "http://ptolemy.eecs.berkeley.edu/xml/models/";
2222
2223    /** Last status message clearing task. */
2224    private TimerTask _lastStatusMessageClearingTask = null;
2225
2226    /** Indicator that the menu has been populated. */
2227    private boolean _menuPopulated = false;
2228
2229    /** Indicator that the data represented in the window has been modified. */
2230    private boolean _modified = false;
2231
2232    /** True if we have printed the securityException message. */
2233    private static boolean _printedSecurityExceptionMessage = false;
2234
2235    /** Timer used for status messages. */
2236    private Timer _statusMessageTimer;
2237}