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}