001/* Top-level window for Ptolemy models with a menubar and status bar.
002
003 Copyright (c) 1998-2018 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026 */
027package ptolemy.actor.gui;
028
029import java.awt.FileDialog;
030import java.io.File;
031import java.io.IOException;
032import java.net.URL;
033import java.util.Iterator;
034import java.util.LinkedList;
035import java.util.List;
036
037import javax.swing.JFileChooser;
038
039import ptolemy.actor.CompositeActor;
040import ptolemy.actor.Manager;
041import ptolemy.data.expr.FileParameter;
042import ptolemy.gui.ComponentDialog;
043import ptolemy.gui.ExtensionFilenameFilter;
044import ptolemy.gui.Query;
045import ptolemy.kernel.CompositeEntity;
046import ptolemy.kernel.undo.UndoStackAttribute;
047import ptolemy.kernel.util.Attribute;
048import ptolemy.kernel.util.BasicModelErrorHandler;
049import ptolemy.kernel.util.ChangeRequest;
050import ptolemy.kernel.util.IllegalActionException;
051import ptolemy.kernel.util.InternalErrorException;
052import ptolemy.kernel.util.KernelException;
053import ptolemy.kernel.util.NamedObj;
054
055///////////////////////////////////////////////////////////////////
056//// PtolemyFrame
057
058/**
059 This is a top-level window for Ptolemy models with a menubar and status bar.
060 Derived classes should add components to the content pane using a
061 line like:
062 <pre>
063 getContentPane().add(component, BorderLayout.CENTER);
064 </pre>
065 This extends the base class by associating with it a Ptolemy II model
066 or object and specifying a model error handler for that model
067 that handles model errors by throwing an exception.
068 <p>
069 If the model contains an instance of FileParameter named "_help", then
070 the file or URL specified by that attribute will be opened when "Help"
071 in the Help menu is invoked.
072
073 @author Edward A. Lee
074 @version $Id$
075 @since Ptolemy II 1.0
076 @Pt.ProposedRating Green (eal)
077 @Pt.AcceptedRating Yellow (johnr)
078 */
079@SuppressWarnings("serial")
080public abstract class PtolemyFrame extends TableauFrame {
081    /** Construct a frame associated with the specified Ptolemy II model.
082     *  After constructing this, it is necessary
083     *  to call setVisible(true) to make the frame appear.
084     *  This is typically done by calling show() on the controlling tableau.
085     *  @see Tableau#show()
086     *  @param model The model to put in this frame, or null if none.
087     */
088    public PtolemyFrame(NamedObj model) {
089        this(model, null);
090    }
091
092    /** Construct a frame associated with the specified Ptolemy II model
093     *  or object. After constructing this, it is necessary
094     *  to call setVisible(true) to make the frame appear.
095     *  This is typically done by calling show() on the controlling tableau.
096     *  @see Tableau#show()
097     *  @param model The model or object to put in this frame, or null if none.
098     *  @param tableau The tableau responsible for this frame, or null if none.
099     */
100    public PtolemyFrame(NamedObj model, Tableau tableau) {
101        super(tableau);
102
103        // Add .xml and .moml to the list of extensions.
104        // Note that extensions are matched in a case-insenstive
105        // manner, so this will also match .MoML and .XML.
106        LinkedList extensions = new LinkedList();
107        extensions.add("xml");
108        extensions.add("moml");
109        // We use a constructor that takes a list because
110        // _fileFilter is declared in Top to be a javax.swing.filechooser.FileFilter.
111        _fileFilter = new ExtensionFilenameFilter(extensions);
112
113        setModel(model);
114
115        // Set the window properties if there is an attribute in the
116        // model specifying them.  Errors are ignored.
117        try {
118            WindowPropertiesAttribute properties = (WindowPropertiesAttribute) model
119                    .getAttribute("_windowProperties",
120                            WindowPropertiesAttribute.class);
121
122            if (properties != null) {
123                properties.setProperties(this);
124            }
125        } catch (IllegalActionException ex) {
126            // Ignore.
127        }
128    }
129
130    ///////////////////////////////////////////////////////////////////
131    ////                         public methods                    ////
132
133    /** Expand all the rows of the library.
134     *  Expanding all the rows is useful for testing.
135     *  In this baseclass, this method merely returns.
136     *  In a derived class, this method should expand all the library
137     *  rows in the configuration.
138     */
139    public void expandAllLibraryRows() {
140        // This method is here so that HTMLAbout does not depend on
141        // vergil BasicGraphFrame
142    }
143
144    /** Override the base class to check to see whether the effigy
145     *  is still the valid one for the associated model. If it is
146     *  not, create a new effigy for the model and associate the
147     *  tableau with that effigy.  If the effigy has been marked
148     *  as non-persistent, then a new effigy is not created.
149     *  @return The effigy for the model, or null if none exists.
150     */
151    @Override
152    public Effigy getEffigy() {
153        Effigy originalEffigy = super.getEffigy();
154        if (originalEffigy instanceof PtolemyEffigy) {
155            if (!getTableau().isMaster() && !originalEffigy.masterEffigy()
156                    .equals(originalEffigy.topEffigy())
157            // GT View can set the Effigy as non-persistent so
158            // that the model can be run and the user is not
159            // prompted to save the optimized version.  To
160            // replicate, run $PTII/bin/vergil
161            // ~/ptII/ptolemy/actor/gt/demo/ConstOptimization/ConstOptimization.xml
162            // and then close the optimized model.  You should
163            // not be prompted for save.
164                    && originalEffigy.isPersistent()) {
165                // The tableau is no longer the master, perhaps there
166                // was a deletion.  Hence, the original effigy should
167                // no longer be the associated effigy.
168                //
169                // This code is necessary to solve a problem with deleting an
170                // open composite actor, see:
171                // http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4053
172                // Also, try clicking on a codegen attribute:
173                // https://chess.eecs.berkeley.edu/bugzilla/show_bug.cgi?id=273
174                try {
175                    PtolemyEffigy newEffigy = new PtolemyEffigy(
176                            (CompositeEntity) originalEffigy.getContainer(),
177                            originalEffigy.getContainer()
178                                    .uniqueName(_model.getName()));
179                    newEffigy.setModel(_model);
180                    newEffigy.setModified(originalEffigy.isModified());
181                    getTableau().setContainer(newEffigy);
182                    return newEffigy;
183                } catch (KernelException e) {
184                    throw new InternalErrorException(e);
185                }
186            }
187        }
188        return originalEffigy;
189    }
190
191    /** Get the associated model or Ptolemy II object.
192     *  This can be a CompositeEntity or an EditorIcon, and possibly
193     *  other Ptolemy II objects.
194     *  @return The associated model or object.
195     *  @see #setModel(NamedObj)
196     */
197    public NamedObj getModel() {
198        return _model;
199    }
200
201    /** Set the associated model.  This also sets an error handler for
202     *  the model that results in model errors throwing an exception
203     *  and associates an undo stack with the model.
204     *  @param model The associated model.
205     *  @see #getModel()
206     */
207    public void setModel(NamedObj model) {
208        if (model == null) {
209            if (_model != null) {
210                _model.setModelErrorHandler(null);
211                _model = null;
212            }
213        } else {
214            _model = model;
215            if (model.getContainer() == null) {
216                if (model.getModelErrorHandler() == null) {
217                    _model.setModelErrorHandler(new BasicModelErrorHandler());
218                }
219            }
220
221            List attrList = _model.attributeList(UndoStackAttribute.class);
222
223            if (attrList.size() == 0) {
224                // Create and attach a new instance
225                try {
226                    new UndoStackAttribute(_model, "_undoInfo");
227                } catch (KernelException e) {
228                    throw new InternalErrorException(e);
229                }
230            }
231        }
232    }
233
234    ///////////////////////////////////////////////////////////////////
235    ////                         protected methods                 ////
236
237    /** Clear the current contents.  First, check to see whether
238     *  the contents have been modified, and if so, then prompt the user
239     *  to save them.  A return value of false
240     *  indicates that the user has canceled the action.
241     *  @return False if the user cancels the clear.
242     */
243    @Override
244    protected boolean _clear() {
245        if (super._clear()) {
246            setModel(new CompositeEntity());
247            return true;
248        } else {
249            return false;
250        }
251    }
252
253    /** Close the window.  Look for any Dialogs that are open and close those
254     *  first. If a DialogTableau returns false then it means that the user
255     *  has cancelled the close operation.
256     *  @return False if the user cancels on a save query.
257     */
258    @Override
259    protected boolean _close() {
260        if (_debugClosing) {
261            System.out.println("PtolemyFrame._close() : " + this.getName());
262        }
263
264        // If we generated documentation and are closing a DocEffigy,
265        // then getEffigy() will return an Effigy, not a PtolemyEffigy.
266        Effigy effigy = getEffigy();
267
268        // The effigy should not be null, but if the window has
269        // already been closed somehow, then it will be.
270        if (effigy != null) {
271            List tableaux = effigy.entityList(Tableau.class);
272            Iterator tableauxIterator = tableaux.iterator();
273
274            while (tableauxIterator.hasNext()) {
275                Tableau tableau = (Tableau) tableauxIterator.next();
276
277                if (tableau instanceof DialogTableau) {
278                    DialogTableau dialogTableau = (DialogTableau) tableau;
279
280                    if (!dialogTableau.close()) {
281                        return false;
282                    }
283                }
284            }
285        }
286
287        return super._close();
288    }
289
290    /** Dispose of this frame.
291     *     Override this dispose() method to unattach any listeners that may keep
292     *  this model from getting garbage collected.  This method invokes the
293     *  dispose() method of the superclass,
294     *  {@link ptolemy.actor.gui.TableauFrame}.
295     */
296    @Override
297    public void dispose() {
298        if (_debugClosing) {
299            System.out.println("PtolemyFrame.dispose() : " + this.getName());
300        }
301
302        setModel(null);
303        super.dispose();
304    }
305
306    /** Display more detailed information than given by _about().
307     *  If the model contains an instance of FileParameter named "_help",
308     *  that the file or URL given by that attribute is opened.  Otherwise,
309     *  a built-in generic help file is opened.
310     */
311    @Override
312    protected void _help() {
313        try {
314            FileParameter helpAttribute = (FileParameter) getModel()
315                    .getAttribute("_help", FileParameter.class);
316            URL doc = helpAttribute.asURL();
317            getConfiguration().openModel(null, doc, doc.toExternalForm());
318        } catch (Exception ex) {
319            super._help();
320        }
321    }
322
323    /** Print the contents.  If this frame implements either the
324     *  Printable or Pageable then those interfaces are used to print
325     *  it.  This overrides the base class to queue a change request to do
326     *  the printing, because otherwise, printing will cause a deadlock.
327     */
328    @Override
329    protected void _print() {
330        if (_model != null) {
331            ChangeRequest request = new PrintChangeRequest(this, "Print");
332
333            _model.requestChange(request);
334        } else {
335            super._print();
336        }
337    }
338
339    /** Query the user for a filename, save the model to that file,
340     *  and open a new window to view the model.
341     *  If setModel() has been called, then the initial filename
342     *  is set to the name of the model.  If setModel() has not yet
343     *  been called, then the initial filename to
344     *  <code>model.xml</code>.
345     *  If the model is not idle or paused, we first pause it before
346     *  calling the parent _saveAs() method and then resume when
347     *  we return from the parent _saveAs() method.
348     *  @return True if the save succeeds.
349     */
350    @Override
351    protected boolean _saveAs() {
352        if (_model != null) {
353            // Use the name of the top level by default.
354            _initialSaveAsFileName = _model.toplevel().getName() + ".xml";
355
356            if (_initialSaveAsFileName.length() == 4) {
357                // Useless model name (empty string).
358                _initialSaveAsFileName = "model.xml";
359            }
360        } else {
361            _initialSaveAsFileName = "model.xml";
362        }
363
364        // If the model is not idle or paused, then pause it while saving
365        // This solves bug where if we have Const -> MonitorValue with
366        // SDFDirector with default parameters and run it and then do
367        // SaveAs, we got strange behaviour.
368        if (_model instanceof CompositeActor) {
369            Manager manager = ((CompositeActor) _model).getManager();
370
371            if (manager != null) {
372                Manager.State state = manager.getState();
373
374                if (state == Manager.IDLE && state == Manager.PAUSED) {
375                    return super._saveAs();
376                } else {
377                    manager.pause();
378
379                    boolean returnValue = super._saveAs();
380                    manager.resume();
381                    return returnValue;
382                }
383            }
384        }
385
386        // If the user saves a file without an extension, we force .xml.
387        return _saveAs(".xml");
388    }
389
390    /** Create and return a file dialog for the "Save As" command.
391     *  This overrides the base class to add options to the dialog.
392     *  If {@link ptolemy.gui.PtGUIUtilities#useFileDialog()} returns false,
393     *  then {@link ptolemy.gui.Top#_saveAs()} uses this method.  Otherwise,
394     *  {@link #_saveAsFileDialogComponent()} is used.
395
396     *  @return A file dialog for save as.
397     */
398    @Override
399    protected JFileChooser _saveAsJFileChooserComponent() {
400        JFileChooser fileChooser = super._saveAsJFileChooserComponent();
401
402        if (_model != null && _model.getContainer() != null) {
403            _query = new Query();
404            _query.addCheckBox("submodel", "Save submodel only", false);
405            fileChooser.setAccessory(_query);
406        }
407
408        return fileChooser;
409    }
410
411    /** Create and return a file dialog for the "Save As" command.
412     *  This overrides the base class to add options to the dialog.
413     *  If {@link ptolemy.gui.PtGUIUtilities#useFileDialog()} returns true
414     *  then {@link ptolemy.gui.Top#_saveAs()} uses this method.  Otherwise,
415     *  {@link #_saveAsJFileChooserComponent()} is used.
416
417     *  @return A file dialog for save as.
418     */
419    @Override
420    protected FileDialog _saveAsFileDialogComponent() {
421        FileDialog fileDialog = super._saveAsFileDialogComponent();
422
423        if (_model != null && _model.getContainer() != null) {
424            _query = new Query();
425            _query.addCheckBox("submodel", "Save submodel only", false);
426            // The problem here is that with FileDialog, we can't add the
427            // query as an accessory like we can with JFileChooser.  So, we
428            // pop up a check box dialog before bringing up the FileDialog.
429            ComponentDialog dialog = new ComponentDialog(this, "Save Submodel?",
430                    _query);
431            String button = dialog.buttonPressed();
432
433            if (button.equals("Cancel")) {
434                return null;
435            }
436        }
437
438        return fileDialog;
439    }
440
441    /** Write the model to the specified file.  This method delegates
442     *  to the top effigy containing the associated Tableau, if there
443     *  is one, and otherwise throws an exception. This ensures that the
444     *  data written is the description of the entire model, not just
445     *  the portion within some composite actor.   It also adjusts the
446     *  URIAttribute in the model to match the specified file, if
447     *  necessary, and creates one otherwise.  It also
448     *  overrides the base class to update the attributes if they need
449     *  to update their content.
450     *  @param file The file to write to.
451     *  @exception IOException If the write fails.
452     */
453    @Override
454    protected void _writeFile(File file) throws IOException {
455        Tableau tableau = getTableau();
456
457        if (tableau != null) {
458            Effigy effigy = (Effigy) tableau.getContainer();
459
460            if (effigy != null) {
461                // Update all the attributes that need updated.
462                if (_model != null) {
463                    Iterator attributes = _model.attributeList(Attribute.class)
464                            .iterator();
465
466                    while (attributes.hasNext()) {
467                        Attribute attribute = (Attribute) attributes.next();
468                        attribute.updateContent();
469                    }
470                }
471
472                // Ensure that if we do ever try to call this method,
473                // that it is the top effigy that is written.
474                // If there is no model, delegate to the top effigy.
475                // Otherwise, delegate to the effigy corresponding
476                // to the top-level of the model (which may not be
477                // the same as the top effigy, e.g. when using
478                // ModelReference). An exception is that if we
479                // in a saveAs command (_query != null) and the
480                // user has requested saving the submodel, then
481                // we do no delegating.
482                if (_model == null) {
483                    effigy = effigy.topEffigy();
484                } else if (_query == null || _model.getContainer() != null
485                        && _query.hasEntry("submodel")
486                        && !_query.getBooleanValue("submodel")) {
487                    effigy = effigy.masterEffigy();
488                }
489
490                effigy.writeFile(file);
491                return;
492            }
493        }
494
495        throw new IOException("Cannot find an effigy to delegate writing.");
496    }
497
498    /** The query used to specify save as options. */
499    protected Query _query;
500
501    ///////////////////////////////////////////////////////////////////
502    ////                         inner classes                     ////
503
504    /** A ChangeRequest for calling the _print() method. */
505    class PrintChangeRequest extends ChangeRequest {
506        public PrintChangeRequest(Object source, String description) {
507            super(source, description);
508        }
509
510        @Override
511        protected void _execute() throws Exception {
512            PtolemyFrame.super._print();
513        }
514    }
515
516    ///////////////////////////////////////////////////////////////////
517    ////                         private variables                 ////
518
519    // The model that this window controls, if any.
520    private NamedObj _model;
521}