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