/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.tools;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.JToolBar;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.tree.TreePath;

import org.opensourcephysics.controls.OSPLog;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.controls.XMLControlElement;
import org.opensourcephysics.display.DisplayRes;
import org.opensourcephysics.display.OSPRuntime;
import org.opensourcephysics.display.PrintUtils;

/**
 * This provides a GUI for building LaunchNode and LaunchSet xml files.
 *
 * @author Douglas Brown
 * @version 1.2
 */
public class LaunchBuilder extends Launcher {
  // static constants
  static final Color RED = new Color(255, 102, 102);
  // static fields
  static JFileChooser fileChooser;
  static javax.swing.filechooser.FileFilter jarFileFilter;
  static javax.swing.filechooser.FileFilter htmlFileFilter;
  static javax.swing.filechooser.FileFilter allFileFilter;
  static int maxArgs = 4; // max number of main method args
  static File ospJarFolder;
  static Color enabledColor, disabledColor;
  // instance fields
  Action newTabSetAction; // creates a new tabset with a single tab
  Action changeAction;    // loads input data into selected node
  Action newTabAction;    // creates a new root node and tab
  Action addAction;       // adds new node to selected node
  Action cutAction;       // cuts selected node to clipboard
  Action copyAction;      // copies selected node to clipboard
  Action pasteAction;     // adds clipboard node to selected node
  Action importAction;    // adds xml file node to selected node
  Action saveAsAction;    // saves selected node to a new file
  Action saveAction;      // saves selected node
  Action saveAllAction;   // saves tabset and all nodes
  Action saveSetAsAction; // saves tabset and all nodes to new files
  Action moveUpAction;    // moves selected node up
  Action moveDownAction;  // moves selected node down
  Action openJarAction;   // opens a jar file and sets node path
  Action searchJarAction; // searches the current jars for a launchable class
  Action saveJarAction;   // saves a jar file containing the current tab set
  Action openArgAction;   // opens an xml file and sets the current argument
  Action openModelArgAction; // opens an xml file and sets the current model argument
  Action openURLAction;   // opens an html file and sets url
  Action searchJarForModelAction;   // searches the current jars for a model class
  Action openTabAction;   // opens a node in a new tab
  Icon openIcon;
  JTabbedPane editorTabs;
  FocusListener focusListener;
  KeyListener keyListener;
  JPanel displayPanel;
  JPanel launchPanel;
  JPanel authorPanel;
  ArrayList<JLabel> labels;
  JTextField titleField;
  JLabel titleLabel;
  JTextField passwordEditor;
  JLabel passwordLabel;
  JTextField nameField;
  JLabel nameLabel;
  JTextField tooltipField;
  JLabel tooltipLabel;
  TitledBorder displayTitle;
  JTextField classField;
  JLabel classLabel;
  JTextField argField;
  JLabel argLabel;
  JSpinner argSpinner;
  JTextField jarField;
  JLabel jarLabel;
  JToolBar displayBar;
  JLabel displayLabel;
  SpinnerNumberModel displaySpinnerModel;
  JSpinner displaySpinner;
  JLabel pathLabel;
  JTextField pathField;
  JButton openDisplayChooserButton;
  JButton showModelArgsButton;
  JDialog modelArgsDialog;
  JTextField modelArgField;
  JLabel modelArgLabel;
  JSpinner modelArgSpinner;
  JButton modelArgCloseButton;
  JButton modelArgClearButton;
  JLabel tabTitleLabel;
  JTextField tabTitleField;
  JPanel urlPanel;
  JTextPane descriptionPane;
  JScrollPane descriptionScroller;
  TitledBorder descriptionTitle;
  JEditorPane htmlPane;
  JScrollPane htmlScroller;
  JSplitPane displaySplitPane;
  JTextField authorField;
  JLabel authorLabel;
  JTextField keywordField;
  JLabel keywordLabel;
  JTextField levelField;
  JLabel levelLabel;
  JTextField languagesField;
  JLabel languagesLabel;
  JTextPane commentPane;
  JScrollPane commentScroller;
  TitledBorder commentTitle;
  TitledBorder optionsTitle;
  TitledBorder securityTitle;
  JCheckBox editorEnabledCheckBox;
  JCheckBox encryptCheckBox;
  JCheckBox onEditCheckBox;
  JCheckBox onLoadCheckBox;
  JCheckBox hideRootCheckBox;
  JCheckBox hiddenCheckBox;
  JCheckBox buttonViewCheckBox;
  JCheckBox singleVMCheckBox;
  JCheckBox showLogCheckBox;
  JCheckBox clearLogCheckBox;
  JCheckBox singletonCheckBox;
  JCheckBox singleAppCheckBox;
  JComboBox levelDropDown;
  String previousClassPath;
  JButton newTabButton;
  JButton addButton;
  JButton cutButton;
  JButton copyButton;
  JButton pasteButton;
  JButton moveUpButton;
  JButton moveDownButton;
  JMenuItem newItem;
  JMenuItem previewItem;
  JMenuItem saveNodeItem;
  JMenuItem saveNodeAsItem;
  JMenuItem saveSetAsItem;
  JMenuItem saveAllItem;
  JMenuItem saveJarItem;
  JMenuItem importItem;
  JMenuItem openTabItem;
  JMenu toolsMenu;
  JMenuItem encryptionToolItem;
  JToolBar toolbar;
  LaunchSaver saver = new LaunchSaver(this);

  static {
//  	LaunchClassChooser.setBasePath("C:/Documents and Settings/DoBrown/My Documents/Eclipse/workspace_deploy");
//  	Launcher.setJarsOnly(false);
  	enabledColor = UIManager.getColor("Label.foreground"); //$NON-NLS-1$
  	if (enabledColor==null)
  		enabledColor = Color.BLACK;
  	disabledColor = UIManager.getColor("Label.disabledForeground"); //$NON-NLS-1$
  	if (disabledColor==null)
    	disabledColor = UIManager.getColor("Label.disabledText"); //$NON-NLS-1$
  	if (disabledColor==null)
  		disabledColor = Color.LIGHT_GRAY;
  }

  /**
   * No-arg constructor.
   */
  public LaunchBuilder() {
    OSPRuntime.setAuthorMode(true);
    XML.setLoader(NodeSet.class, new NodeSet.Loader());
  }

  /**
   * Constructs a builder and loads the specified file.
   *
   * @param fileName the file name
   */
  public LaunchBuilder(String fileName) {
    super(fileName);
    OSPRuntime.setAuthorMode(true);
    XML.setLoader(NodeSet.class, new NodeSet.Loader());
  }

  /**
   * Constructs a builder with or without a splash screen.
   *
   * @param splash true to show the splash screen
   */
  public LaunchBuilder(boolean splash) {
    super(splash);
    OSPRuntime.setAuthorMode(true);
    XML.setLoader(NodeSet.class, new NodeSet.Loader());
  }

  /**
   * Constructs a builder and loads the specified file with or without splash.
   *
   * @param fileName the file name
   * @param splash true to show the splash screen
   */
  public LaunchBuilder(String fileName, boolean splash) {
    super(fileName, splash);
    OSPRuntime.setAuthorMode(true);
    XML.setLoader(NodeSet.class, new NodeSet.Loader());
  }

  /**
   * Main entry point when used as application.
   *
   * @param args args[0] may be an xml file name
   */
  public static void main(String[] args) {
    //    java.util.Locale.setDefault(new java.util.Locale("es"));
    //    OSPLog.setLevel(ConsoleLevel.ALL);
    //    OSPRuntime.setJavaLookAndFeel(true);
    String fileName = null;
    if((args!=null)&&(args.length!=0)) {
      fileName = args[0];
    }
    LaunchBuilder builder = new LaunchBuilder(fileName);
    builder.frame.setVisible(true);
  }

  /**
   * Saves a node to the specified file.
   *
   * @param node the node
   * @param fileName the desired name of the file
   * @return the name of the saved file, or null if not saved
   */
  public String save(LaunchNode node, String fileName) {
    if(node==null) {
      return null;
    }
    if((fileName==null)||fileName.trim().equals("")) { //$NON-NLS-1$
      return saveAs(node);
    }
    // add .xml extension if none but don't require it
    if(XML.getExtension(fileName)==null) {
      while(fileName.endsWith(".")) { //$NON-NLS-1$
        fileName = fileName.substring(0, fileName.length()-1);
      }
      fileName += ".xml";             //$NON-NLS-1$
    }
    if(!saveOwnedNodes(node)) {
      return null;
    }
    String filepath = fileName;
    if(!tabSetBasePath.equals("")) { //$NON-NLS-1$
      filepath = XML.getResolvedPath(fileName, tabSetBasePath);
    } else {
      String jarBase = OSPRuntime.getLaunchJarDirectory();
      filepath = XML.getResolvedPath(fileName, jarBase);
    }
    File file = new File(filepath);
    OSPLog.fine(fileName+" = "+file.getAbsolutePath()); //$NON-NLS-1$
    String fullName = XML.forwardSlash(file.getAbsolutePath());
    String path = XML.getDirectoryPath(fullName);
    XML.createFolders(path);
    XMLControlElement control = new XMLControlElement(node);
    control.write(fullName);
    if(!control.canWrite) {
      OSPLog.info(LaunchRes.getString("Dialog.SaveFailed.Message")+" "+fullName);                        //$NON-NLS-1$//$NON-NLS-2$
      JOptionPane.showMessageDialog(null, LaunchRes.getString("Dialog.SaveFailed.Message")+" "+fileName, //$NON-NLS-1$//$NON-NLS-2$
        LaunchRes.getString("Dialog.SaveFailed.Title"), //$NON-NLS-1$
          JOptionPane.WARNING_MESSAGE);
      return null;
    }
    node.setFileName(fileName);
    changedFiles.remove(node.getFileName());
    return fileName;
  }

  /**
   * Saves a node to an xml file selected with a chooser.
   *
   * @param node the node
   * @return the name of the file
   */
  public String saveAs(LaunchNode node) {
    getXMLChooser().setFileFilter(xmlFileFilter);
    if(node.getFileName()!=null) {
      String name = XML.getResolvedPath(node.getFileName(), tabSetBasePath);
      getXMLChooser().setSelectedFile(new File(name));
    } else {
      String name = node.name;
      if(name.equals(LaunchRes.getString("NewNode.Name"))||           //$NON-NLS-1$
        name.equals(LaunchRes.getString("NewTab.Name"))) {            //$NON-NLS-1$
        name = LaunchRes.getString("NewFile.Name");                   //$NON-NLS-1$
      }
      String path = XML.getResolvedPath(name+".xml", tabSetBasePath); //$NON-NLS-1$
      getXMLChooser().setSelectedFile(new File(path));
    }
    int result = getXMLChooser().showDialog(null, LaunchRes.getString("FileChooser.SaveAs.Title")); //$NON-NLS-1$
    if(result==JFileChooser.APPROVE_OPTION) {
      File file = getXMLChooser().getSelectedFile();
      // create folder structure
      String path = XML.forwardSlash(file.getParent());
      XML.createFolders(path);
      // check to see if file already exists
      if(file.exists()) {
        // prevent duplicate file names
        String name = XML.forwardSlash(file.getAbsolutePath());
        name = XML.getPathRelativeTo(name, tabSetBasePath);
        if(getOpenPaths().contains(name)) {
          JOptionPane.showMessageDialog(frame, LaunchRes.getString("Dialog.DuplicateFileName.Message")+" \""+name+"\"", //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
            LaunchRes.getString("Dialog.DuplicateFileName.Title"),                                                 //$NON-NLS-1$
              JOptionPane.WARNING_MESSAGE);
          return null;
        }
        int selected = JOptionPane.showConfirmDialog(frame, LaunchRes.getString("Dialog.ReplaceFile.Message")+" "+ //$NON-NLS-1$//$NON-NLS-2$
          file.getName()+XML.NEW_LINE+LaunchRes.getString("Dialog.ReplaceFile.Question"), //$NON-NLS-1$
            LaunchRes.getString("Dialog.ReplaceFile.Title"),                              //$NON-NLS-1$
              JOptionPane.YES_NO_OPTION);
        if(selected!=JOptionPane.YES_OPTION) {
          return null;
        }
      }
      path = XML.forwardSlash(file.getAbsolutePath());
      String fileName = XML.getPathRelativeTo(path, tabSetBasePath);
      OSPRuntime.chooserDir = XML.getDirectoryPath(path);
      // get clones before saving
      Map<LaunchPanel, LaunchNode> clones = getClones(node);
      path = save(node, fileName);
      if(path!=null) {
        if(node.isRoot()) {
          // refresh title of root tab
          for(int i = 0; i<tabbedPane.getTabCount(); i++) {
            LaunchPanel tab = (LaunchPanel) tabbedPane.getComponentAt(i);
            if(tab.getRootNode()==node) {
              tabbedPane.setTitleAt(i, node.toString());
              break;
            }
          }
        }
        // refresh clones
        for(Iterator<LaunchPanel> it = clones.keySet().iterator(); it.hasNext(); ) {
          LaunchPanel cloneTab = it.next();
          LaunchNode clone = clones.get(cloneTab);
          clone.setFileName(node.getFileName());
          // refresh title of clone tab
          if(clone==cloneTab.getRootNode()) {
            int n = tabbedPane.indexOfComponent(cloneTab);
            tabbedPane.setTitleAt(n, node.toString());
          }
        }
        if(tabSetName!=null) {
          changedFiles.add(tabSetName);
        }
      }
      return path;
    }
    return null;
  }

  // ______________________________ protected methods _____________________________

  /**
   * Saves the owned nodes of the specified node.
   *
   * @param node the node
   * @return true unless cancelled by user
   */
  protected boolean saveOwnedNodes(LaunchNode node) {
    if(node==null) {
      return false;
    }
    if(node.isSelfContained()) {
      return true; // owned nodes saved within node
    }
    LaunchNode[] nodes = node.getChildOwnedNodes();
    for(int i = 0; i<nodes.length; i++) {
      // save owned nodes of owned node, if any
      if(nodes[i].getChildOwnedNodes().length>1) {
        if(!saveOwnedNodes(nodes[i])) {
          return false;
        }
      }
      if(save(nodes[i], nodes[i].getFileName())==null) {
        return false;
      }
    }
    return true;
  }

  /**
   * Saves a tabset to a file selected with a chooser.
   *
   * @return the absolute path of the tabset file
   */
  protected String saveTabSetAs() {
    saver.setBuilder(this);
    saver.setVisible(true);
    if(!saver.isApproved()) {
      return null;
    }
    String fileName = XML.getResolvedPath(tabSetName, tabSetBasePath);
    File file = new File(fileName);
    if(file.exists()) {
      int selected = JOptionPane.showConfirmDialog(frame, LaunchRes.getString("Dialog.ReplaceFile.Message")+" "+ //$NON-NLS-1$//$NON-NLS-2$
        file.getName()+XML.NEW_LINE+LaunchRes.getString("Dialog.ReplaceFile.Question"), //$NON-NLS-1$
          LaunchRes.getString("Dialog.ReplaceFile.Title"),                              //$NON-NLS-1$
            JOptionPane.YES_NO_OPTION);
      if(selected!=JOptionPane.YES_OPTION) {
        return null;
      }
    }
    saveState = saver.saveStateCheckBox.isSelected();
    return saveTabSet();
  }

  /**
   * Saves the current tabset.
   *
   * @return the full path to the saved file, or null if not saved
   */
  public String saveTabSet() {
    if(tabSetName==null) {
      return null;
    }
    if(tabSetName.trim().equals("")) { //$NON-NLS-1$
      return saveTabSetAs();
    }
    // check for read-only files
    if(!isTabSetWritable()) {
      return saveTabSetAs();
    }
    if(!selfContained&&!saveTabs()) {
      return null;
    }
    // save tab set
    String fileName = tabSetName;
    if(!tabSetBasePath.equals("")) { //$NON-NLS-1$
      fileName = XML.getResolvedPath(tabSetName, tabSetBasePath);
    } else {
      String jarBase = OSPRuntime.getLaunchJarDirectory();
      fileName = XML.getResolvedPath(tabSetName, jarBase);
    }
    OSPLog.fine(fileName);
    File file = new File(fileName);
    fileName = XML.forwardSlash(file.getAbsolutePath());
    String path = XML.getDirectoryPath(fileName);
    XML.createFolders(path);
    LaunchSet tabset = new LaunchSet(this, tabSetName);
    XMLControlElement control = new XMLControlElement(tabset);
    if(control.write(fileName)==null) {
      return null;
    }
    changedFiles.clear();
    jarBasePath = null; // signals tabset is now a file
    if(spawner!=null) {
      spawner.open(fileName);
      spawner.refreshGUI();
    }
    return fileName;
  }

  /**
   * Saves tabs.
   * @return true unless cancelled by user
   */
  public boolean saveTabs() {
    Component[] tabs = tabbedPane.getComponents();
    for(int i = 0; i<tabs.length; i++) {
      LaunchPanel tab = (LaunchPanel) tabs[i];
      LaunchNode root = tab.getRootNode();
      if((root.getFileName()==null)||root.getFileName().equals("")) { //$NON-NLS-1$
        continue;
      }
      save(root, XML.getResolvedPath(root.getFileName(), tabSetBasePath));
    }
    return true;
  }

  /**
   * Refreshes the selected node.
   */
  protected void refreshSelectedNode() {
    refreshNode(getSelectedNode());
  }

  /**
   * Refreshes the specified node with data from the input fields.
   *
   * @param node the node to refresh
   */
  protected void refreshNode(LaunchNode node) {
    boolean changed = false;
    if(node!=null) {
      if(node.isSingleVM()!=singleVMCheckBox.isSelected()) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeVM"));                                          //$NON-NLS-1$
        LaunchNode parent = (LaunchNode) node.getParent();
        if((parent!=null)&&parent.isSingleVM()) {
          node.singleVM = false;
          node.singleVMOff = !singleVMCheckBox.isSelected();
        } else {
          node.singleVM = singleVMCheckBox.isSelected();
          node.singleVMOff = false;
        }
        if(node.isSingleVM()) {
          showLogCheckBox.setSelected(node.showLog);
          clearLogCheckBox.setSelected(node.clearLog);
          singleAppCheckBox.setSelected(node.isSingleApp());
        } else {
          singletonCheckBox.setSelected(node.singleton);
        }
        changed = true;
      }
      if(node.isSingleVM()&&(node.showLog!=showLogCheckBox.isSelected())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeShowLog"));                                     //$NON-NLS-1$
        node.showLog = showLogCheckBox.isSelected();
        changed = true;
      }
      if(node.isSingleVM()&&node.isShowLog()&&(node.clearLog!=clearLogCheckBox.isSelected())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeClearLog"));                                    //$NON-NLS-1$
        node.clearLog = clearLogCheckBox.isSelected();
        changed = true;
      }
      if(node.isSingleVM()&&(node.isSingleApp()!=singleAppCheckBox.isSelected())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeSingleApp"));                                   //$NON-NLS-1$
        LaunchNode parent = (LaunchNode) node.getParent();
        if((parent!=null)&&parent.isSingleApp()) {
          node.singleApp = false;
          node.singleAppOff = !singleAppCheckBox.isSelected();
        } else {
          node.singleApp = singleAppCheckBox.isSelected();
          node.singleAppOff = false;
        }
        changed = true;
      }
      if(node.singleton!=singletonCheckBox.isSelected()) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeSingleton"));                                   //$NON-NLS-1$
        node.singleton = singletonCheckBox.isSelected();
        changed = true;
      }
      if(node.hiddenInLauncher!=hiddenCheckBox.isSelected()) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeHidden"));                                      //$NON-NLS-1$
        node.hiddenInLauncher = hiddenCheckBox.isSelected();
        changed = true;
      }
      if(node.isButtonView()!=buttonViewCheckBox.isSelected()) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeButtonView"));                                      //$NON-NLS-1$
        node.setButtonView(buttonViewCheckBox.isSelected());
        changed = true;
      }
      if(!node.name.equals(nameField.getText())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeName"));                                        //$NON-NLS-1$
        node.name = nameField.getText();
        changed = true;
      }
      if(!node.tooltip.equals(tooltipField.getText())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeTooltip"));                                         //$NON-NLS-1$
        node.tooltip = tooltipField.getText();
        changed = true;
      }
      if(!node.description.equals(descriptionPane.getText())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeDesc"));                                        //$NON-NLS-1$
        node.description = descriptionPane.getText();
        changed = true;
      }
      int n = ((Integer) argSpinner.getValue()).intValue();
      String arg = argField.getText();
      if(!arg.equals("")) {                                                                                      //$NON-NLS-1$
        node.setMinimumArgLength(n+1);
      }
      if((node.args.length>n)&&!arg.equals(node.args[n])) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeArgs")+" "+n);                                  //$NON-NLS-1$//$NON-NLS-2$
        node.args[n] = arg;
        if(arg.equals("")) {                                                                                     //$NON-NLS-1$
          node.setMinimumArgLength(1);
        }
        changed = true;
      }
      String jarPath = jarField.getText();
      if((jarPath.equals("")&&(node.classPath!=null))||(!jarPath.equals("")&&!jarPath.equals(node.classPath))) { //$NON-NLS-1$//$NON-NLS-2$
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodePath"));                                          //$NON-NLS-1$
        node.setClassPath(jarPath.equals("")                                                                       //$NON-NLS-1$
                          ? null : jarPath);
        changed = true;
      }
      n = ((Integer) displaySpinner.getValue()).intValue();
      LaunchNode.DisplayTab displayTab = node.getDisplayTab(n);
      String input = pathField.getText();
      if (displayTab==null
      		||((displayTab.path!=null &&!displayTab.path.equals(input))
      		||(input!=null && !input.equals(displayTab.path)))) {
        String title = (displayTab==null) ? null : displayTab.title;
        String[] args = (displayTab==null) ? null : displayTab.getModelArgs();
        node.setDisplayTab(n, title, input, args);
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeURL"));                                  //$NON-NLS-1$
        changed = true;
      }
      if (displayTab!=null 
      		&& displayTab.modelClass != null 
      		&& modelArgsDialog.isVisible()) {
        n = ((Integer) modelArgSpinner.getValue()).intValue();
        arg = modelArgField.getText();
        if(!arg.equals("")) {                                                                                      //$NON-NLS-1$
          displayTab.setMinimumModelArgLength(n+1);
        }
        String[] args = displayTab.getModelArgs();
        if(args.length>n && !arg.equals(args[n])) {
          OSPLog.finest(LaunchRes.getString("Log.Message.ChangeModelArgs")+" "+n);                                  //$NON-NLS-1$//$NON-NLS-2$
          args[n] = arg.equals("")? null: arg; //$NON-NLS-1$
        	displayTab.setModelArgs(args);
          if(arg.equals("")) {                                                                                     //$NON-NLS-1$
            displayTab.setMinimumModelArgLength(0);
          }          
          changed = true;
        }
      }
      input = tabTitleField.getText();
      if(input.equals("")) {                                                                                       //$NON-NLS-1$
        input = null;
      }
      if(displayTab!= null 
      		&& ((displayTab.title!=null && !displayTab.title.equals(input)) 
      		|| (input!=null && !input.equals(displayTab.title)))) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeHTMLTabTitle"));                                  //$NON-NLS-1$
        node.setDisplayTab(n, input, displayTab.path, displayTab.getModelArgs());
        changed = true;
      }
      String className = classField.getText();
      if(className.equals("")) {                                                                                   //$NON-NLS-1$
        if(node.launchClassName!=null) {
          node.launchClassName = null;
          node.launchClass = null;
          node.launchModelScroller = null;
          OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeLaunchClass"));                                 //$NON-NLS-1$
          changed = true;
        }
      } else if(!className.equals(node.launchClassName)||(!className.equals("")&&(node.getLaunchClass()==null))) { //$NON-NLS-1$
        boolean change = node.setLaunchClass(className);
        if(change) {
          OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeLaunchClass"));             //$NON-NLS-1$
          changed = true;
        }
      }
      if(!node.getAuthor().equals(authorField.getText())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeAuthor"));                    //$NON-NLS-1$
        node.author = authorField.getText();
        changed = true;
      }
      if(!node.keywords.equals(keywordField.getText())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeKeywords"));                  //$NON-NLS-1$
        node.keywords = keywordField.getText();
        changed = true;
      }
      if(!node.level.equals(levelField.getText())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeLevel"));                     //$NON-NLS-1$
        node.level = levelField.getText();
        changed = true;
      }
      if(!node.languages.equals(languagesField.getText())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeLanguages"));                 //$NON-NLS-1$
        node.languages = languagesField.getText();
        changed = true;
      }
      if(!node.comment.equals(commentPane.getText())) {
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeComment"));                   //$NON-NLS-1$
        node.comment = commentPane.getText();
        changed = true;
      }
      LaunchNode root = (LaunchNode) node.getRoot();
      if(root!=null) {
        boolean hide = hideRootCheckBox.isSelected();
        if(hide!=root.hiddenWhenRoot) {
          root.hiddenWhenRoot = hide;
          OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeRootHidden"));              //$NON-NLS-1$
          changed = true;
        }
        boolean edit = editorEnabledCheckBox.isSelected();
        if(edit!=editorEnabled) {
          editorEnabled = edit;
          OSPLog.finest(LaunchRes.getString("Log.Message.ChangeNodeEditorEnabled"));           //$NON-NLS-1$
          if(tabSetName!=null) {
            changedFiles.add(tabSetName);
          }
          refreshGUI();
        }
      }
      if(changed) {
        OSPLog.fine(LaunchRes.getString("Log.Message.ChangeNode")+" \""+node.toString()+"\""); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
        LaunchPanel tab = getSelectedTab();
        if(tab!=null) {
          tab.treeModel.nodeChanged(node);
        }
        if(node.getOwner()!=null) {
          changedFiles.add(node.getOwner().getFileName());
        } else {
          changedFiles.add(tabSetName);
        }
        refreshClones(node);
        refreshGUI();
      }
    }
  }

  /**
   * Creates a LaunchPanel with the specified root and adds it to a new tab.
   *
   * @param root the root node
   * @return true if tab successfully added
   */
  public boolean addTab(LaunchNode root) {
    if(root==null) {
      return false;
    }
    OSPLog.finest(root.toString());
    boolean added = super.addTab(root);
    if(added) {
      if(tabSetName==null) {
        tabSetName = LaunchRes.getString("Tabset.Name.New"); //$NON-NLS-1$
      }
      changedFiles.add(tabSetName);
      refreshGUI();
    }
    return added;
  }

  /**
   * Removes the selected tab. Overrides Launcher method.
   *
   * @return true if the tab was removed
   */
  public boolean removeSelectedTab() {
    // close the tabset if only one tab is open
    if(tabbedPane.getTabCount()==1) {
      String[] prevArgs = undoManager.getLauncherState();
      boolean removed = removeAllTabs();
      if(removed&&(prevArgs!=null)) {
        // null new args indicate not redoable
        LauncherUndo.LoadEdit edit = undoManager.new LoadEdit(null, prevArgs);
        undoSupport.postEdit(edit);
      }
      return removed;
    }
    // check for unsaved changes in the selected tab
    LaunchPanel tab = (LaunchPanel) tabbedPane.getSelectedComponent();
    if(tab!=null) {
      if(!saveChanges(tab)) {
        return false;
      }
    }
    boolean removed = super.removeSelectedTab();
    if((tabSetName!=null)&&removed) {
      changedFiles.add(tabSetName);
      refreshGUI();
    }
    return removed;
  }

  /**
   * Offers to save changes, if any, to the specified tab.
   *
   * @param tab the tab
   * @return true unless cancelled by user
   */
  protected boolean saveChanges(LaunchPanel tab) {
    LaunchNode root = tab.getRootNode();
    int n = tabbedPane.indexOfComponent(tab);
    String name = (n>-1) ? tabbedPane.getTitleAt(n) : getDisplayName(root.getFileName());
    boolean changed = changedFiles.contains(root.getFileName());
    LaunchNode[] nodes = root.getAllOwnedNodes();
    for(int i = 0; i<nodes.length; i++) {
      changed = changed||changedFiles.contains(nodes[i].getFileName());
    }
    if(changed) {
      int selected = JOptionPane.showConfirmDialog(frame, LaunchRes.getString("Dialog.SaveChanges.Tab.Message")+" \""+ //$NON-NLS-1$//$NON-NLS-2$
        name+"\""+XML.NEW_LINE+                               //$NON-NLS-1$
          LaunchRes.getString("Dialog.SaveChanges.Question"), //$NON-NLS-1$
            LaunchRes.getString("Dialog.SaveChanges.Title"),  //$NON-NLS-1$
              JOptionPane.YES_NO_CANCEL_OPTION);
      if(selected==JOptionPane.CANCEL_OPTION) {
        return false;
      }
      if(selected==JOptionPane.YES_OPTION) {
        // save root and all owned nodes
        save(root, root.getFileName());
      }
    }
    return true;
  }

  /**
   * Removes all tabs and closes the tabset.
   *
   * @return true if all tabs were removed
   */
  protected boolean removeAllTabs() {
    if(!saveAllChanges()) {
      return false;
    }
    return super.removeAllTabs();
  }

  /**
   * Offers to save all changes, if any.
   *
   * @return true unless cancelled by user
   */
  protected boolean saveAllChanges() {
    // save changes to tab set
    if(!changedFiles.isEmpty()&&(tabbedPane.getTabCount()>0)) {
      String message = LaunchRes.getString("Dialog.SaveChanges.Tabset.Message"); //$NON-NLS-1$
      int selected = JOptionPane.showConfirmDialog(frame, message+"\""+          //$NON-NLS-1$
        tabSetName+"\""+XML.NEW_LINE+                                            //$NON-NLS-1$
          LaunchRes.getString("Dialog.SaveChanges.Question"),                    //$NON-NLS-1$
            LaunchRes.getString("Dialog.SaveChanges.Title"),                     //$NON-NLS-1$
              JOptionPane.YES_NO_CANCEL_OPTION);
      if(selected==JOptionPane.CANCEL_OPTION) {
        return false;
      }
      if(selected==JOptionPane.YES_OPTION) {
        if(tabSetName.equals(LaunchRes.getString("Tabset.Name.New"))||           //$NON-NLS-1$
                  !saveAllItem.isEnabled()) {
          saveTabSetAs();
        } else {
          saveTabSet();
        }
      }
    }
    return true;
  }

  /**
   * Refreshes string resources.
   */
  protected void refreshStringResources() {
    super.refreshStringResources();
    saver = new LaunchSaver(this);
    editorTabs.setTitleAt(0, LaunchRes.getString("Tab.Display"));                 //$NON-NLS-1$
    editorTabs.setTitleAt(1, LaunchRes.getString("Tab.Launch"));                  //$NON-NLS-1$
    editorTabs.setTitleAt(2, LaunchRes.getString("Tab.Author"));                  //$NON-NLS-1$
    displayTitle.setTitle(LaunchRes.getString("Label.DisplayPane"));              //$NON-NLS-1$
    commentTitle.setTitle(LaunchRes.getString("Label.Comments"));                 //$NON-NLS-1$
    descriptionTitle.setTitle(LaunchRes.getString("Label.Description"));          //$NON-NLS-1$
    optionsTitle.setTitle(LaunchRes.getString("Label.Options"));                  //$NON-NLS-1$
    hiddenCheckBox.setText(LaunchRes.getString("Checkbox.Hidden"));               //$NON-NLS-1$
    buttonViewCheckBox.setText(LaunchRes.getString("Checkbox.ButtonView"));       //$NON-NLS-1$
    nameLabel.setText(LaunchRes.getString("Label.Name"));                         //$NON-NLS-1$
    tooltipLabel.setText(LaunchRes.getString("Label.Tooltip"));                   //$NON-NLS-1$
    displayLabel.setText(LaunchRes.getString("Label.Display"));                   //$NON-NLS-1$
    tabTitleLabel.setText(LaunchRes.getString("Label.TabTitle"));         				//$NON-NLS-1$
    pathLabel.setText(LaunchRes.getString("Label.Path"));                          //$NON-NLS-1$
    jarLabel.setText(LaunchRes.getString("Label.Jar"));                           //$NON-NLS-1$
    classLabel.setText(LaunchRes.getString("Label.Class"));                       //$NON-NLS-1$
    argLabel.setText(LaunchRes.getString("Label.Args"));                          //$NON-NLS-1$
    singleVMCheckBox.setText(LaunchRes.getString("Checkbox.SingleVM"));           //$NON-NLS-1$
    showLogCheckBox.setText(LaunchRes.getString("Checkbox.ShowLog"));             //$NON-NLS-1$
    clearLogCheckBox.setText(LaunchRes.getString("Checkbox.ClearLog"));           //$NON-NLS-1$
    singletonCheckBox.setText(LaunchRes.getString("Checkbox.Singleton"));         //$NON-NLS-1$
    singleAppCheckBox.setText(LaunchRes.getString("Checkbox.SingleApp"));         //$NON-NLS-1$
    authorLabel.setText(LaunchRes.getString("Label.Author"));                     //$NON-NLS-1$
    keywordLabel.setText(LaunchRes.getString("Label.Keywords"));                  //$NON-NLS-1$
    levelLabel.setText(LaunchRes.getString("Label.Level"));                       //$NON-NLS-1$
    languagesLabel.setText(LaunchRes.getString("Label.Languages"));               //$NON-NLS-1$
    securityTitle.setTitle(LaunchRes.getString("Label.Security"));                //$NON-NLS-1$
    editorEnabledCheckBox.setText(LaunchRes.getString("Checkbox.EditorEnabled")); //$NON-NLS-1$
    encryptCheckBox.setText(LaunchRes.getString("Checkbox.Encrypted"));           //$NON-NLS-1$
    passwordLabel.setText(LaunchRes.getString("Label.Password"));                 //$NON-NLS-1$
    onLoadCheckBox.setText(LaunchRes.getString("Checkbox.PWLoad"));               //$NON-NLS-1$
    titleLabel.setText(LaunchRes.getString("Label.Title"));                       //$NON-NLS-1$
    hideRootCheckBox.setText(LaunchRes.getString("Checkbox.HideRoot"));           //$NON-NLS-1$
    previewItem.setText(LaunchRes.getString("Menu.File.Preview"));                //$NON-NLS-1$
    encryptionToolItem.setText(LaunchRes.getString("MenuItem.EncryptionTool"));   //$NON-NLS-1$
    newItem.setText(LaunchRes.getString("Menu.File.New"));                        //$NON-NLS-1$
    importItem.setText(LaunchRes.getString("Action.Import"));                     //$NON-NLS-1$
    saveNodeItem.setText(LaunchRes.getString("Action.SaveNode"));                 //$NON-NLS-1$
    saveNodeAsItem.setText(LaunchRes.getString("Action.SaveNodeAs"));             //$NON-NLS-1$
    saveAllItem.setText(LaunchRes.getString("Action.SaveAll"));                   //$NON-NLS-1$
    openTabItem.setText(LaunchRes.getString("Action.OpenTab"));                   //$NON-NLS-1$
    saveSetAsItem.setText(LaunchRes.getString("Action.SaveSetAs"));               //$NON-NLS-1$
    toolsMenu.setText(LaunchRes.getString("Menu.Tools"));                         //$NON-NLS-1$
    newTabButton.setText(LaunchRes.getString("Action.New"));                      //$NON-NLS-1$
    addButton.setText(LaunchRes.getString("Action.Add"));                         //$NON-NLS-1$
    cutButton.setText(LaunchRes.getString("Action.Cut"));                         //$NON-NLS-1$
    copyButton.setText(LaunchRes.getString("Action.Copy"));                       //$NON-NLS-1$
    pasteButton.setText(LaunchRes.getString("Action.Paste"));                     //$NON-NLS-1$
    moveUpButton.setText(LaunchRes.getString("Action.Up"));                       //$NON-NLS-1$
    moveDownButton.setText(LaunchRes.getString("Action.Down"));                   //$NON-NLS-1$
    showModelArgsButton.setText(LaunchRes.getString("Button.ModelArgs"));         //$NON-NLS-1$
    modelArgsDialog.setTitle(LaunchRes.getString("Dialog.ModelArgs.Title"));      //$NON-NLS-1$
    modelArgCloseButton.setText(LaunchRes.getString("Dialog.Button.Close"));      //$NON-NLS-1$
    modelArgClearButton.setText(LaunchRes.getString("Dialog.Button.Clear"));      //$NON-NLS-1$
    modelArgLabel.setText(LaunchRes.getString("Label.Args"));                     //$NON-NLS-1$
    // set tool tips
    pathField.setToolTipText(LaunchRes.getString("Display.Path.Tooltip"));         //$NON-NLS-1$
    tabTitleField.setToolTipText(LaunchRes.getString("Display.Tab.Title.Tooltip")); //$NON-NLS-1$
    displaySpinner.setToolTipText(LaunchRes.getString("Display.Tab.Number.Tooltip"));//$NON-NLS-1$
    openDisplayChooserButton.setToolTipText(LaunchRes.getString("Button.OpenDisplay.Tooltip")); //$NON-NLS-1$
    showModelArgsButton.setToolTipText(LaunchRes.getString("Button.ModelArgs.Tooltip")); //$NON-NLS-1$
    // reset label sizes
    labels.clear();
    labels.add(nameLabel);
    labels.add(tooltipLabel);
    labels.add(displayLabel);
    // set label sizes
    FontRenderContext frc = new FontRenderContext(null, false, false); 
    Font font = nameLabel.getFont();
    //display panel labels
    int w = 0;
    for(Iterator<JLabel> it = labels.iterator(); it.hasNext(); ) {
      JLabel next = it.next();
      Rectangle2D rect = font.getStringBounds(next.getText()+" ", frc); //$NON-NLS-1$
      w = Math.max(w, (int) rect.getWidth()+1);
    }
    Dimension labelSize = new Dimension(w, 20);
    for(Iterator<JLabel> it = labels.iterator(); it.hasNext(); ) {
      JLabel next = it.next();
      next.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2));
      next.setPreferredSize(labelSize);
      next.setHorizontalAlignment(SwingConstants.TRAILING);
    }
    // launch panel labels
    labels.clear();
    labels.add(jarLabel);
    labels.add(classLabel);
    labels.add(argLabel);
    w = 0;
    for(Iterator<JLabel> it = labels.iterator(); it.hasNext(); ) {
      JLabel next = it.next();
      Rectangle2D rect = font.getStringBounds(next.getText()+" ", frc); //$NON-NLS-1$
      w = Math.max(w, (int) rect.getWidth()+1);
    }
    labelSize = new Dimension(w, 20);
    for(Iterator<JLabel> it = labels.iterator(); it.hasNext(); ) {
      JLabel next = it.next();
      next.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2));
      next.setPreferredSize(labelSize);
      next.setHorizontalAlignment(SwingConstants.TRAILING);
    }
    labels.clear();
    // author panel labels
    labels.add(authorLabel);
    labels.add(keywordLabel);
    labels.add(levelLabel);
    labels.add(languagesLabel);
    w = 0;
    for(Iterator<JLabel> it = labels.iterator(); it.hasNext(); ) {
      JLabel next = it.next();
      Rectangle2D rect = font.getStringBounds(next.getText()+" ", frc); //$NON-NLS-1$
      w = Math.max(w, (int) rect.getWidth()+1);
    }
    labelSize = new Dimension(w, 20);
    for(Iterator<JLabel> it = labels.iterator(); it.hasNext(); ) {
      JLabel next = it.next();
      next.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2));
      next.setPreferredSize(labelSize);
      next.setHorizontalAlignment(SwingConstants.TRAILING);
    }
  }

  /**
   * Refreshes the GUI.
   */
  protected void refreshGUI() {
    if(previousNode!=null) { // new tab has been selected
      LaunchNode prev = previousNode;
      previousNode = null;
      refreshNode(prev);
    }
    if(newNodeSelected) {
      argSpinner.setValue(new Integer(0));
      displaySpinner.setValue(new Integer(0));
      newNodeSelected = false;
    }
    LaunchNode root = getRootNode();
    boolean rootEnabled = root==null || root.enabled;
    if (!rootEnabled)
    	editorTabs.setSelectedIndex(0);
    titleField.setText(title);
    titleField.setBackground(Color.white);
    super.refreshGUI();
    // refresh frame title
    String theTitle = frame.getTitle();
    if(this.title!=null) {
      if(!changedFiles.isEmpty()) {
        theTitle += " ["+tabSetName+"*]"; //$NON-NLS-1$//$NON-NLS-2$
      } else {
        theTitle += " ["+tabSetName+"]";  //$NON-NLS-1$//$NON-NLS-2$
      }
    } else if(!changedFiles.isEmpty()) {
      if(tabbedPane.getTabCount()==0) {
        changedFiles.clear();
      } else {
        theTitle += "*";                  //$NON-NLS-1$
      }
    }
    frame.setTitle(theTitle);
    // refresh selected node items
    final LaunchNode node = getSelectedNode();
    if(node!=null) {
      // refresh tab titles
      for(int i = 0; i<tabbedPane.getTabCount(); i++) {
        root = getTab(i).getRootNode();
        tabbedPane.setTitleAt(i, root.toString());
      }
      // refresh display tab
      hiddenCheckBox.setSelected(node.isHiddenInLauncher());
      boolean parentHidden = (node.getParent()!=null)&&((LaunchNode) node.getParent()).isHiddenInLauncher();
      hiddenCheckBox.setEnabled(!parentHidden && rootEnabled);
      nameField.setText(node.toString());
      nameField.setBackground(Color.white);
      tooltipField.setText(node.tooltip);
      tooltipField.setBackground(Color.white);
      descriptionPane.setText(node.description);
      descriptionPane.setBackground(Color.white);
      int n = ((Integer) displaySpinner.getValue()).intValue();
      LaunchNode.DisplayTab displayTab = node.getDisplayTab(n);
      String urlPath = (displayTab!=null)? displayTab.path: null;
      boolean badURL = (urlPath!=null && displayTab.url==null && displayTab.modelClass==null);
      pathField.setText(urlPath);
      pathField.setBackground(badURL ? RED : Color.white);
      displaySpinnerModel.setMaximum(new Integer(node.getDisplayTabCount()));
      displaySpinner.setVisible(node.getDisplayTab(0)!=null);
      boolean hasURL = (displayTab!=null)&&(displayTab.path!=null);
      tabTitleLabel.setVisible(hasURL);
      tabTitleField.setText((displayTab!=null) ? displayTab.title : null);
      tabTitleField.setBackground(Color.white);
      displayBar.remove(showModelArgsButton);
      if(displayTab!=null 
      		&& (displayTab.url!=null 
      		|| displayTab.getModelScroller()!=null)) {
	      if(displayTab.url!=null) {
	      	displaySplitPane.setTopComponent(htmlScroller);
	      	if (htmlPane.getPage()!=displayTab.url) {
		        try {
		          if(displayTab.url.getContent()!=null) {
		            final java.net.URL url = displayTab.url;
		            Runnable runner = new Runnable() {
		              public void run() {
		                try {
		                  htmlPane.setPage(url);
		                } catch(IOException ex) {
		                  OSPLog.fine(LaunchRes.getString("Log.Message.BadURL")+" "+url); //$NON-NLS-1$//$NON-NLS-2$
		                }
		              }
		
		            };
		            SwingUtilities.invokeLater(runner);
		          }
		        } catch(IOException ex) {
		          htmlPane.setText(null);
		        }
	      	}
	      }
	      else if (displayTab.getModelScroller()!= null) {
	      	JScrollPane scroller = displayTab.getModelScroller();
	      	scroller.setBorder(displayTitle);
          displaySplitPane.setTopComponent(scroller);
          // add modelArgsButton at end
          displayBar.add(showModelArgsButton);
          n = ((Integer) modelArgSpinner.getValue()).intValue();
          if(displayTab.modelArgs.length>n) {
            modelArgField.setText(displayTab.modelArgs[n]);
          } else {
          	modelArgField.setText(""); //$NON-NLS-1$
          }
          boolean xmlArg = modelArgField.getText().endsWith(".xml"); //$NON-NLS-1$
          Resource res = null;
          if(xmlArg) {
            res = ResourceLoader.getResource(modelArgField.getText());
            modelArgField.setBackground((res==null) ? RED : Color.white);
          } else {
          	modelArgField.setBackground(Color.white);
          }
	      }
      }
      else {
      	if (n == 0 && node.getLaunchModelScroller() != null) {
        	JScrollPane scroller = node.getLaunchModelScroller();
      		scroller.setBorder(displayTitle);
          displaySplitPane.setTopComponent(scroller);
        } 
      	else {
          displaySplitPane.setTopComponent(htmlScroller);
          htmlPane.setContentType(LaunchPanel.TEXT_TYPE);
          htmlPane.setText(null);
	      }
      }
      displayBar.validate();
      // refresh launch node
      // check the path and update the class chooser
      String path = node.getClassPath(); // in node-to-root order
      if(!path.equals(previousClassPath)) {
        boolean success = getClassChooser().setPath(path);
        searchJarAction.setEnabled(success);
        searchJarForModelAction.setEnabled(success);
      }
      // store path for later comparison
      previousClassPath = node.getClassPath();
      jarField.setText(node.classPath);
      jarField.setBackground(((node.classPath!=null)&&!getClassChooser().isLoaded(node.classPath)) ? RED : Color.white);
      classField.setText(node.launchClassName);
      classField.setBackground(((node.getLaunchClass()==null)&&(node.launchClassName!=null)) ? RED : Color.white);
      n = ((Integer) argSpinner.getValue()).intValue();
      if(node.args.length>n) {
        argField.setText(node.args[n]);
      } else {
        argField.setText(""); //$NON-NLS-1$
      }
      boolean xmlArg = argField.getText().endsWith(".xml"); //$NON-NLS-1$
      Resource res = null;
      if(xmlArg) {
        res = ResourceLoader.getResource(argField.getText());
        argField.setBackground((res==null) ? RED : Color.white);
      } else {
        argField.setBackground(Color.white);
      }
      // set enabled and selected states of checkboxes
      LaunchNode parent = (LaunchNode) node.getParent(); // may be null
      singletonCheckBox.setEnabled((parent==null)||!parent.isSingleton());
      singletonCheckBox.setSelected(node.isSingleton());
      singleVMCheckBox.setSelected(node.isSingleVM());
      if(node.isSingleVM()) {
        showLogCheckBox.setEnabled((parent==null)||!parent.isShowLog());
        showLogCheckBox.setSelected(node.isShowLog());
        clearLogCheckBox.setEnabled((parent==null)||!parent.isClearLog());
        clearLogCheckBox.setSelected(node.isClearLog());
        singleAppCheckBox.setEnabled(true);
        singleAppCheckBox.setSelected(node.isSingleApp());
      } else {
        showLogCheckBox.setEnabled(false);
        showLogCheckBox.setSelected(false);
        clearLogCheckBox.setEnabled(false);
        clearLogCheckBox.setSelected(false);
        singleAppCheckBox.setEnabled(false);
        singleAppCheckBox.setSelected(false);
      }
      levelDropDown.setVisible(node.isShowLog());
      clearLogCheckBox.setVisible(node.isShowLog());
      // refresh the level dropdown if visible
      if(levelDropDown.isVisible()) {
        boolean useAll = ((parent==null)||!parent.isShowLog());
        // disable during refresh to prevent triggering events
        levelDropDown.setEnabled(false);
        levelDropDown.removeAllItems();
        for(int i = 0; i<OSPLog.levels.length; i++) {
          if(useAll||(OSPLog.levels[i].intValue()<=parent.getLogLevel().intValue())) {
            levelDropDown.addItem(OSPLog.levels[i]);
          }
        }
        levelDropDown.setSelectedItem(node.getLogLevel());
        levelDropDown.setEnabled(true);
      }
      // refresh author tab
      authorField.setText(node.getAuthor());
      authorField.setBackground(Color.white);
      keywordField.setText(node.keywords);
      keywordField.setBackground(Color.white);
      levelField.setText(node.level);
      levelField.setBackground(Color.white);
      languagesField.setText(node.languages);
      languagesField.setBackground(Color.white);
      commentPane.setText(node.comment);
      commentPane.setBackground(Color.white);
      boolean hasPW = (password!=null)&&!password.equals(""); //$NON-NLS-1$
      onLoadCheckBox.setEnabled(hasPW);
      onLoadCheckBox.setSelected(hasPW&&pwRequiredToLoad);
      encryptCheckBox.setSelected(password!=null);
      passwordEditor.setEnabled(encryptCheckBox.isSelected());
      passwordLabel.setEnabled(encryptCheckBox.isSelected());
      passwordEditor.setText(password);
      passwordEditor.setBackground(Color.white);
    }
    // rebuild file menu
    fileMenu.removeAll();
    fileMenu.add(newItem);
    if(undoManager.canReload()) {
      fileMenu.add(backItem);
    }
    fileMenu.addSeparator();
    fileMenu.add(openItem);
    if(openFromJarMenu!=null) {
      fileMenu.add(openFromJarMenu);
    }
    LaunchPanel tab = getSelectedTab();
    if (tab!=null && !tab.getRootNode().enabled) {
      fileMenu.add(passwordItem);
    }
    boolean isZipped = (jarBasePath!=null)&&!jarBasePath.equals(""); //$NON-NLS-1$
    saveAllItem.setEnabled(!isZipped&&isTabSetWritable());
    if(tab!=null) {
    	if (rootEnabled) fileMenu.add(importItem);
      fileMenu.addSeparator();
      if (rootEnabled) fileMenu.add(closeTabItem);
      fileMenu.add(closeAllItem);
      fileMenu.addSeparator();
      fileMenu.add(previewItem);
      if (rootEnabled) {
	      fileMenu.addSeparator();
	      fileMenu.add(saveAllItem);
	      fileMenu.add(saveSetAsItem);
	      if(OSPRuntime.getLaunchJarName()!=null) {
	        fileMenu.addSeparator();
	        fileMenu.add(saveJarItem);
	      }
      }
      // added by W. Christian to capture screen
      JMenu printMenu = new JMenu(DisplayRes.getString("DrawingFrame.Print_menu_title"));                          //$NON-NLS-1$
      JMenuItem printFrameItem = new JMenuItem(DisplayRes.getString("DrawingFrame.PrintFrame_menu_item"));         //$NON-NLS-1$
      JMenuItem saveFrameAsEPSItem = new JMenuItem(DisplayRes.getString("DrawingFrame.SaveFrameAsEPS_menu_item")); //$NON-NLS-1$
      printMenu.add(printFrameItem);
      printMenu.add(saveFrameAsEPSItem);
      fileMenu.add(printMenu);
      // print action
      printFrameItem.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          PrintUtils.printComponent(frame);
        }

      });
      // save as EPS action
      saveFrameAsEPSItem.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          try {
            PrintUtils.saveComponentAsEPS(frame);
          } catch(IOException ex) {
            /** empty block */
          }
        }

      });
      // end W. Christian code
      // set tab properties
      frame.getContentPane().add(toolbar, BorderLayout.NORTH);
      tab.dataPanel.add(editorTabs, BorderLayout.CENTER);
      // hidden root
      if(getRootNode().getChildCount()==0) {
        getRootNode().hiddenWhenRoot = false;
        hideRootCheckBox.setEnabled(false);
      } else {
        hideRootCheckBox.setEnabled((node==null)||!node.isButtonView());
      }
      if (!rootEnabled) 
      	hideRootCheckBox.setEnabled(false);
      boolean rootVisible = !getRootNode().hiddenWhenRoot;
      hideRootCheckBox.setSelected(!rootVisible);
      tab.tree.setRootVisible(rootVisible);
      if((getSelectedNode()==null)&&!rootVisible) {
        tab.setSelectedNode((LaunchNode) getRootNode().getChildAt(0));
      }
      // button view
      buttonViewCheckBox.setSelected((node!=null)&&node.isButtonView());
      buttonViewCheckBox.setEnabled(rootVisible && rootEnabled);
      // editor enabled
      editorEnabledCheckBox.setSelected(editorEnabled);
      // rootEnabled effects
      editorTabs.setEnabled(rootEnabled);
      nameField.setEnabled(rootEnabled);
      tooltipField.setEnabled(rootEnabled);
      descriptionPane.setEnabled(rootEnabled);
      pathField.setEnabled(rootEnabled);
      titleField.setEnabled(rootEnabled);
      displaySpinner.setEnabled(rootEnabled);
      openDisplayChooserButton.setEnabled(rootEnabled);
      titleLabel.setEnabled(rootEnabled);
      nameLabel.setEnabled(rootEnabled);
      tooltipLabel.setEnabled(rootEnabled);
      displayLabel.setEnabled(rootEnabled);
      pathLabel.setEnabled(rootEnabled);
      newTabButton.setEnabled(rootEnabled);
      addButton.setEnabled(rootEnabled);
      cutButton.setEnabled(rootEnabled);
      copyButton.setEnabled(rootEnabled);
      pasteButton.setEnabled(rootEnabled);
      moveUpButton.setEnabled(rootEnabled);
      moveDownButton.setEnabled(rootEnabled);
      tabTitleField.setEnabled(rootEnabled);
    	htmlScroller.setEnabled(rootEnabled);
      showModelArgsButton.setEnabled(rootEnabled);
      displayTitle.setTitleColor(rootEnabled? enabledColor: disabledColor);
      descriptionTitle.setTitleColor(rootEnabled? enabledColor: disabledColor);
    } else { // no tab
      frame.getContentPane().remove(toolbar);
    }
    if(exitItem!=null) {
      fileMenu.addSeparator();
      fileMenu.add(exitItem);
    }
    // update tabbed pane tool tips and icons
    for(int k = 0; k<tabbedPane.getTabCount(); k++) {
      root = getTab(k).getRootNode();
      if(root.getFileName()!=null) {
        tabbedPane.setIconAt(k, getFileIcon(root));
        tabbedPane.setToolTipTextAt(k, LaunchRes.getString("ToolTip.FileName")+" \""+root.getFileName()+"\""); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
      } else {
        tabbedPane.setIconAt(k, null);
        tabbedPane.setToolTipTextAt(k, null);
      }
    }
  }

  /**
   * Creates the GUI.
   *
   * @param splash true to show the splash screen
   */
  protected void createGUI(boolean splash) {
    wInit = 600;
    hInit = 540;
    labels = new ArrayList<JLabel>();
    super.createGUI(splash);
    // add listeners to refresh GUI
    frame.addWindowListener(new WindowAdapter() {
      // Added by W. Christian to disable the Language menu
      public void windowGainedFocus(WindowEvent e) {
        OSPRuntime.setAuthorMode(true);
      }
      public void windowActivated(WindowEvent e) {
        OSPRuntime.setAuthorMode(true);
      }
      public void windowOpened(WindowEvent e) {
        if(getSelectedNode()!=null) {
          // clear htmlPane to force a full refresh
          htmlPane.setContentType(LaunchPanel.TEXT_TYPE);
          htmlPane.setText(null);
        }
        refreshGUI();
      }

    });
    tabbedPane.addComponentListener(new ComponentAdapter() {
      public void componentResized(ComponentEvent e) {
        refreshGUI();
      }

    });
    // create additional file and folder icons
    String imageFile = "/org/opensourcephysics/resources/tools/images/whitefile.gif"; //$NON-NLS-1$
    whiteFileIcon = loadIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/ghostfile.gif";    //$NON-NLS-1$
    ghostFileIcon = loadIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/redfile.gif";      //$NON-NLS-1$
    redFileIcon = loadIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/yellowfile.gif";   //$NON-NLS-1$
    yellowFileIcon = loadIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/whitefolder.gif";  //$NON-NLS-1$
    whiteFolderIcon = loadIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/redfolder.gif";    //$NON-NLS-1$
    redFolderIcon = loadIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/greenfolder.gif";  //$NON-NLS-1$
    greenFolderIcon = loadIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/yellowfolder.gif"; //$NON-NLS-1$
    yellowFolderIcon = loadIcon(imageFile);
    // create actions
    createActions();
    // create fields
    titleField = new JTextField();
    titleField.addKeyListener(new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        if(e.getKeyCode()==KeyEvent.VK_ENTER) {
          String text = titleField.getText();
          if(text.equals("")) { //$NON-NLS-1$
            text = null;
          }
          if(text!=title) {
            changedFiles.add(tabSetName);
          }
          title = text;
          refreshGUI();
        } else {
          titleField.setBackground(Color.yellow);
        }
      }

    });
    titleField.addFocusListener(new FocusAdapter() {
      public void focusLost(FocusEvent e) {
        String text = titleField.getText();
        if(text.equals("")) { //$NON-NLS-1$
          text = null;
        }
        if(text!=title) {
          changedFiles.add(tabSetName);
        }
        title = text;
        refreshGUI();
      }

    });
    nameField = new JTextField();
    nameField.addKeyListener(keyListener);
    nameField.addFocusListener(focusListener);
    tooltipField = new JTextField();
    tooltipField.addKeyListener(keyListener);
    tooltipField.addFocusListener(focusListener);
    displayTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.DisplayPane")); //$NON-NLS-1$
    classField = new JTextField();
    classField.addKeyListener(keyListener);
    classField.addFocusListener(focusListener);
    argField = new JTextField();
    argField.addKeyListener(keyListener);
    argField.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        if(OSPRuntime.isPopupTrigger(e)) {
          XMLControl control = new XMLControlElement();
          if(control.read(argField.getText())!=null) {
            JPopupMenu popup = new JPopupMenu();
            JMenuItem item = new JMenuItem(LaunchRes.getString("MenuItem.EncryptionTool")); //$NON-NLS-1$
            item.addActionListener(new ActionListener() {
              public void actionPerformed(ActionEvent e) {
                EncryptionTool tool = EncryptionTool.getTool();
                tool.open(argField.getText());
                tool.setVisible(true);
              }

            });
            popup.add(item);
            popup.show(argField, e.getX(), e.getY()+8);
          }
        }
      }

    });
    argField.addFocusListener(focusListener);
    modelArgField = new JTextField();
    modelArgField.addKeyListener(keyListener);
    modelArgField.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        if(OSPRuntime.isPopupTrigger(e)) {
          XMLControl control = new XMLControlElement();
          if(control.read(modelArgField.getText())!=null) {
            JPopupMenu popup = new JPopupMenu();
            JMenuItem item = new JMenuItem(LaunchRes.getString("MenuItem.EncryptionTool")); //$NON-NLS-1$
            item.addActionListener(new ActionListener() {
              public void actionPerformed(ActionEvent e) {
                EncryptionTool tool = EncryptionTool.getTool();
                tool.open(modelArgField.getText());
                tool.setVisible(true);
              }

            });
            popup.add(item);
            popup.show(modelArgField, e.getX(), e.getY()+8);
          }
        }
      }

    });
    modelArgField.addFocusListener(focusListener);
    jarField = new JTextField();
    jarField.addKeyListener(keyListener);
    jarField.addFocusListener(focusListener);
    pathField = new JTextField();
    pathField.addKeyListener(keyListener);
    pathField.addFocusListener(focusListener);
    tabTitleField = new JTextField();
    tabTitleField.addKeyListener(keyListener);
    tabTitleField.addFocusListener(focusListener);
    keywordField = new JTextField();
    keywordField.addKeyListener(keyListener);
    keywordField.addFocusListener(focusListener);
    authorField = new JTextField();
    authorField.addKeyListener(keyListener);
    authorField.addFocusListener(focusListener);
    levelField = new JTextField();
    levelField.addKeyListener(keyListener);
    levelField.addFocusListener(focusListener);
    languagesField = new JTextField();
    languagesField.addKeyListener(keyListener);
    languagesField.addFocusListener(focusListener);
    commentPane = new JTextPane() {
      public void paintComponent(Graphics g) {
        if(OSPRuntime.antiAliasText) {
          Graphics2D g2 = (Graphics2D) g;
          RenderingHints rh = g2.getRenderingHints();
          rh.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
          rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        }
        super.paintComponent(g);
      }

    };
    commentPane.addKeyListener(keyListener);
    commentPane.addFocusListener(focusListener);
    commentScroller = new JScrollPane(commentPane);
    commentTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.Comments")); //$NON-NLS-1$
    commentScroller.setBorder(commentTitle);
    descriptionPane = new JTextPane();
    descriptionPane.addKeyListener(keyListener);
    descriptionPane.addFocusListener(focusListener);
    descriptionScroller = new JScrollPane(descriptionPane);
    descriptionTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.Description")); //$NON-NLS-1$
    descriptionScroller.setBorder(descriptionTitle);
    displaySplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT) {
      public void setDividerLocation(int loc) {
        super.setDividerLocation(loc);
        double divider = getDividerLocation();
        divider /= getHeight()-getDividerSize();
        setName(""+divider); //$NON-NLS-1$
      }
      public void setTopComponent(Component comp) {
      	if (comp == getTopComponent()) return;
        int prev = getLastDividerLocation();
        String divider = getName();
        super.setTopComponent(comp);
        if(divider!=null) {
          double loc = Double.parseDouble(divider);
          loc = Math.max(0.0, loc);
          loc = Math.min(1.0, loc);
          setDividerLocation(loc);
          setLastDividerLocation(prev);
          Runnable runner = new Runnable() {
            public void run() {
              JViewport view = descriptionScroller.getViewport();
              view.setViewPosition(new Point(0, 0));
            }
          };
          SwingUtilities.invokeLater(runner);
        }
      }

    };
    displaySplitPane.setBottomComponent(descriptionScroller);
    displaySplitPane.setOneTouchExpandable(true);
    displaySplitPane.setResizeWeight(1);
    htmlPane = new JTextPane() {
      public void paintComponent(Graphics g) {
        if(OSPRuntime.antiAliasText) {
          Graphics2D g2 = (Graphics2D) g;
          RenderingHints rh = g2.getRenderingHints();
          rh.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
          rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        }
        super.paintComponent(g);
      }

    };
    htmlPane.setEditable(false);
    htmlScroller = new JScrollPane(htmlPane);
    htmlScroller.setBorder(displayTitle);
    displaySplitPane.setTopComponent(htmlScroller);
    hiddenCheckBox = new JCheckBox();
    hiddenCheckBox.addActionListener(changeAction);
    hiddenCheckBox.setContentAreaFilled(false);
    hiddenCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
    buttonViewCheckBox = new JCheckBox();
    buttonViewCheckBox.addActionListener(changeAction);
    buttonViewCheckBox.setContentAreaFilled(false);
    buttonViewCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
    // assemble display panel
    JPanel displayPanel = new JPanel(new BorderLayout());
    JToolBar nameBar = new JToolBar();
    nameBar.setFloatable(false);
    nameLabel = new JLabel();
    labels.add(nameLabel);
    nameBar.add(nameLabel);
    nameBar.add(nameField);
    nameBar.add(hiddenCheckBox);
    displayPanel.add(nameBar, BorderLayout.NORTH);
    JPanel tooltipPanel = new JPanel(new BorderLayout());
    displayPanel.add(tooltipPanel, BorderLayout.CENTER);
    JToolBar tooltipBar = new JToolBar();
    tooltipBar.setFloatable(false);
    tooltipLabel = new JLabel();
    tooltipBar.add(tooltipLabel);
    tooltipBar.add(tooltipField);
    tooltipPanel.add(tooltipBar, BorderLayout.NORTH);
    urlPanel = new JPanel(new BorderLayout());
    tooltipPanel.add(urlPanel, BorderLayout.CENTER);
    displayBar = new JToolBar();
    displayBar.setFloatable(false);
    displayLabel = new JLabel();
    pathLabel = new JLabel();
    pathLabel.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
    tabTitleLabel = new JLabel();
    tabTitleLabel.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
    displaySpinnerModel = new SpinnerNumberModel(0, 0, 1, 1);
    displaySpinner = new JSpinner(displaySpinnerModel);
    JSpinner.NumberEditor editor = new JSpinner.NumberEditor(displaySpinner);
    displaySpinner.setEditor(editor);
    displaySpinner.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        if(pathField.getBackground()==Color.yellow) {
          refreshSelectedNode();
        } else {
          refreshGUI();
        }
      }

    });
    displayBar.add(displayLabel);
    displayBar.add(displaySpinner);
    displayBar.add(pathLabel);
    displayBar.add(pathField);
    openDisplayChooserButton = new JButton(openIcon);
    openDisplayChooserButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      	JPopupMenu openPopup = new JPopupMenu();
      	openPopup.add(openURLAction);
      	openPopup.add(searchJarForModelAction);
      	openPopup.show(openDisplayChooserButton, 0, openDisplayChooserButton.getHeight());
      }
    });
    displayBar.add(openDisplayChooserButton);
    modelArgsDialog = new JDialog(frame, true);
    JToolBar modelArgBar = new JToolBar();
    modelArgBar.setFloatable(false);
    modelArgBar.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 10));
    modelArgLabel = new JLabel();
    modelArgLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 3));
    SpinnerModel model = new SpinnerNumberModel(0, 0, maxArgs-1, 1);
    modelArgSpinner = new JSpinner(model);
    editor = new JSpinner.NumberEditor(modelArgSpinner);
    modelArgSpinner.setEditor(editor);
    modelArgSpinner.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        if(argField.getBackground()==Color.yellow) {
          refreshSelectedNode();
        } else {
          refreshGUI();
        }
      }

    });
    modelArgBar.add(modelArgLabel);
    modelArgBar.add(modelArgSpinner);
    modelArgBar.add(modelArgField);
    modelArgBar.add(openModelArgAction);
    modelArgCloseButton = new JButton(LaunchRes.getString("Dialog.Button.Close")); //$NON-NLS-1$
    modelArgCloseButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
      	modelArgsDialog.setVisible(false);
      }
    });
    modelArgClearButton = new JButton(LaunchRes.getString("Dialog.Button.Clear")); //$NON-NLS-1$
    modelArgClearButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        int n = ((Integer) displaySpinner.getValue()).intValue();
      	LaunchNode node = getSelectedNode();
        LaunchNode.DisplayTab tab = node.getDisplayTab(n);
        if (tab!=null) {
        	tab.setModelArgs(new String[0]);
        	refreshGUI();
        }
      }
    });
    JPanel buttonPanel = new JPanel();    
    buttonPanel.add(modelArgClearButton);
    buttonPanel.add(modelArgCloseButton);
    modelArgsDialog.add(modelArgBar, BorderLayout.CENTER);
    modelArgsDialog.add(buttonPanel, BorderLayout.SOUTH);
    modelArgsDialog.pack();
    Dimension dim = modelArgsDialog.getSize();
    int w = Math.max(dim.width, 240);
    modelArgsDialog.setSize(w, dim.height);
    // button to show the modelArgs dialog
    showModelArgsButton = new JButton();
    showModelArgsButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        Point p0 = new Frame().getLocation();
      	if (modelArgsDialog.getLocation().x == p0.x)
      		modelArgsDialog.setLocationRelativeTo(showModelArgsButton);
      	modelArgsDialog.setVisible(true);
      }
    });
    displayBar.add(tabTitleLabel);
    displayBar.add(tabTitleField);
    urlPanel.add(displayBar, BorderLayout.NORTH);
    urlPanel.add(displaySplitPane, BorderLayout.CENTER);
    // assemble launch panel
    JPanel launchPanel = new JPanel(new BorderLayout());
    JToolBar jarBar = new JToolBar();
    jarBar.setFloatable(false);
    jarLabel = new JLabel();
    labels.add(jarLabel);
    jarBar.add(jarLabel);
    jarBar.add(jarField);
    jarBar.add(openJarAction);
    launchPanel.add(jarBar, BorderLayout.NORTH);
    JPanel classPanel = new JPanel(new BorderLayout());
    launchPanel.add(classPanel, BorderLayout.CENTER);
    JToolBar classBar = new JToolBar();
    classBar.setFloatable(false);
    classLabel = new JLabel();
    labels.add(classLabel);
    classBar.add(classLabel);
    classBar.add(classField);
    classBar.add(searchJarAction);
    classPanel.add(classBar, BorderLayout.NORTH);
    JPanel argPanel = new JPanel(new BorderLayout());
    classPanel.add(argPanel, BorderLayout.CENTER);
    JToolBar argBar = new JToolBar();
    argBar.setFloatable(false);
    argLabel = new JLabel();
    labels.add(argLabel);
    argBar.add(argLabel);
    model = new SpinnerNumberModel(0, 0, maxArgs-1, 1);
    argSpinner = new JSpinner(model);
    editor = new JSpinner.NumberEditor(argSpinner);
    argSpinner.setEditor(editor);
    argSpinner.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        if(argField.getBackground()==Color.yellow) {
          refreshSelectedNode();
        } else {
          refreshGUI();
        }
      }

    });
    argBar.add(argSpinner);
    argBar.add(argField);
    argBar.add(openArgAction);
    argPanel.add(argBar, BorderLayout.NORTH);
    JPanel optionsPanel = new JPanel(new BorderLayout());
    argPanel.add(optionsPanel, BorderLayout.CENTER);
    JToolBar optionsBar = new JToolBar();
    optionsBar.setFloatable(false);
    singleVMCheckBox = new JCheckBox();
    singleVMCheckBox.addActionListener(changeAction);
    singleVMCheckBox.setContentAreaFilled(false);
    singleVMCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
    showLogCheckBox = new JCheckBox();
    showLogCheckBox.addActionListener(changeAction);
    showLogCheckBox.setContentAreaFilled(false);
    showLogCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
    clearLogCheckBox = new JCheckBox();
    clearLogCheckBox.addActionListener(changeAction);
    clearLogCheckBox.setContentAreaFilled(false);
    clearLogCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
    singletonCheckBox = new JCheckBox();
    singletonCheckBox.addActionListener(changeAction);
    singletonCheckBox.setContentAreaFilled(false);
    singletonCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
    singleAppCheckBox = new JCheckBox();
    singleAppCheckBox.addActionListener(changeAction);
    singleAppCheckBox.setContentAreaFilled(false);
    singleAppCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
    levelDropDown = new JComboBox(OSPLog.levels);
    levelDropDown.setMaximumSize(levelDropDown.getMinimumSize());
    levelDropDown.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        if(levelDropDown.isEnabled()) {
          LaunchNode node = getSelectedNode();
          if(node!=null) {
            node.setLogLevel((Level) levelDropDown.getSelectedItem());
          }
        }
      }

    });
    Box checkBoxPanel = Box.createVerticalBox();
    JToolBar bar = new JToolBar();
    bar.setFloatable(false);
    bar.setAlignmentX(Component.LEFT_ALIGNMENT);
    bar.add(singletonCheckBox);
    bar.add(Box.createHorizontalGlue());
    checkBoxPanel.add(bar);
    bar = new JToolBar();
    bar.setFloatable(false);
    bar.setAlignmentX(Component.LEFT_ALIGNMENT);
    bar.add(singleVMCheckBox);
    bar.add(Box.createHorizontalGlue());
    checkBoxPanel.add(bar);
    bar = new JToolBar();
    bar.setFloatable(false);
    bar.setAlignmentX(Component.LEFT_ALIGNMENT);
    bar.add(singleAppCheckBox);
    bar.add(Box.createHorizontalGlue());
    checkBoxPanel.add(bar);
    bar = new JToolBar();
    bar.setFloatable(false);
    bar.add(showLogCheckBox);
    bar.add(Box.createHorizontalStrut(4));
    bar.add(levelDropDown);
    bar.add(Box.createHorizontalStrut(4));
    bar.add(clearLogCheckBox);
    bar.add(Box.createHorizontalGlue());
    bar.setAlignmentX(Component.LEFT_ALIGNMENT);
    checkBoxPanel.add(bar);
    optionsTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.Options")); //$NON-NLS-1$
    Border recess = BorderFactory.createLoweredBevelBorder();
    optionsBar.setBorder(BorderFactory.createCompoundBorder(recess, optionsTitle));
    optionsBar.add(checkBoxPanel);
    optionsPanel.add(optionsBar, BorderLayout.NORTH);
    // create author tab
    JPanel authorPanel = new JPanel(new BorderLayout());
    // put author field on top
    JToolBar authorBar = new JToolBar();
    authorPanel.add(authorBar, BorderLayout.NORTH);
    authorBar.setFloatable(false);
    authorLabel = new JLabel();
    labels.add(authorLabel);
    authorBar.add(authorLabel);
    authorBar.add(authorField);
    // add keyword panel
    JPanel keywordPanel = new JPanel(new BorderLayout());
    authorPanel.add(keywordPanel, BorderLayout.CENTER);
    JToolBar keywordBar = new JToolBar();
    keywordPanel.add(keywordBar, BorderLayout.NORTH);
    keywordBar.setFloatable(false);
    keywordLabel = new JLabel();
    labels.add(keywordLabel);
    keywordBar.add(keywordLabel);
    keywordBar.add(keywordField);
    // add level panel
    JPanel levelPanel = new JPanel(new BorderLayout());
    keywordPanel.add(levelPanel, BorderLayout.CENTER);
    JToolBar levelBar = new JToolBar();
    levelPanel.add(levelBar, BorderLayout.NORTH);
    levelBar.setFloatable(false);
    levelLabel = new JLabel();
    labels.add(levelLabel);
    levelBar.add(levelLabel);
    levelBar.add(levelField);
    // add languages panel
    JPanel languagesPanel = new JPanel(new BorderLayout());
    levelPanel.add(languagesPanel, BorderLayout.CENTER);
    JToolBar languagesBar = new JToolBar();
    languagesPanel.add(languagesBar, BorderLayout.NORTH);
    languagesBar.setFloatable(false);
    languagesLabel = new JLabel();
    labels.add(languagesLabel);
    languagesBar.add(languagesLabel);
    languagesBar.add(languagesField);
    // put security panel in center
    JPanel securityPanel = new JPanel(new BorderLayout());
    languagesPanel.add(securityPanel, BorderLayout.CENTER);
    JToolBar securityBar = new JToolBar();
    securityBar.setFloatable(false);
    securityPanel.add(securityBar, BorderLayout.NORTH);
    securityTitle = BorderFactory.createTitledBorder(LaunchRes.getString("Label.Security")); //$NON-NLS-1$
    securityBar.setBorder(BorderFactory.createCompoundBorder(recess, securityTitle));
    // use vertical box to stack components in security panel
    Box securityBox = Box.createVerticalBox();
    securityBar.add(securityBox);
    // add editorEnabled to box
    bar = new JToolBar();
    bar.setFloatable(false);
    bar.setAlignmentX(Component.LEFT_ALIGNMENT);
    editorEnabledCheckBox = new JCheckBox();
    editorEnabledCheckBox.addActionListener(changeAction);
    editorEnabledCheckBox.setContentAreaFilled(false);
    editorEnabledCheckBox.setAlignmentX(Component.LEFT_ALIGNMENT);
    bar.add(editorEnabledCheckBox);
    bar.add(Box.createHorizontalGlue());
    securityBox.add(bar);
    // add encrypt checkbox to box
    bar = new JToolBar();
    bar.setFloatable(false);
    bar.setAlignmentX(Component.LEFT_ALIGNMENT);
    encryptCheckBox = new JCheckBox();
    encryptCheckBox.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        if(encryptCheckBox.isSelected()&&(password==null)) {
          password = "";                                                   //$NON-NLS-1$
        } else if(!encryptCheckBox.isSelected()) {
          password = null;
        }
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangeEncrypted")); //$NON-NLS-1$
        if(tabSetName!=null) {
          changedFiles.add(tabSetName);
        }
        refreshGUI();
      }

    });
    encryptCheckBox.setContentAreaFilled(false);
    bar.add(encryptCheckBox);
    bar.add(Box.createHorizontalGlue());
    securityBox.add(bar);
    // add password field to box
    bar = new JToolBar();
    bar.setFloatable(false);
    bar.setAlignmentX(Component.LEFT_ALIGNMENT);
    passwordLabel = new JLabel();
    passwordLabel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 2));
    bar.add(passwordLabel);
    passwordEditor = new JTextField();
    passwordEditor.addKeyListener(new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        if(e.getKeyCode()==KeyEvent.VK_ENTER) {
          String text = passwordEditor.getText();
          if(text.equals("")&& //$NON-NLS-1$
            (!encryptCheckBox.isSelected()||!encryptCheckBox.isEnabled())) {
            text = null;
          }
          if(text!=password) {
            changedFiles.add(tabSetName);
          }
          password = text;
          refreshGUI();
        } else {
          passwordEditor.setBackground(Color.yellow);
        }
      }

    });
    bar.add(passwordEditor);
    bar.add(Box.createHorizontalGlue());
    onLoadCheckBox = new JCheckBox();
    onLoadCheckBox.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        pwRequiredToLoad = onLoadCheckBox.isSelected();
        OSPLog.finest(LaunchRes.getString("Log.Message.ChangePWRequirement")); //$NON-NLS-1$
        if(tabSetName!=null) {
          changedFiles.add(tabSetName);
        }
        refreshGUI();
      }

    });
    onLoadCheckBox.setContentAreaFilled(false);
    bar.add(onLoadCheckBox);
    securityBox.add(bar);
    securityPanel.add(commentScroller, BorderLayout.CENTER);
    // create the editor tabs
    editorTabs = new JTabbedPane(SwingConstants.TOP);
    editorTabs.addTab(LaunchRes.getString("Tab.Display"), displayPanel); //$NON-NLS-1$
    editorTabs.addTab(LaunchRes.getString("Tab.Launch"), launchPanel);   //$NON-NLS-1$
    editorTabs.addTab(LaunchRes.getString("Tab.Author"), authorPanel);   //$NON-NLS-1$
    // editorTabs.addTab(LaunchRes.getString("Tab.Server"), serverPanel);
    // create toolbar
    toolbar = new JToolBar();
    toolbar.setFloatable(false);
    toolbar.setRollover(true);
    toolbar.setBorder(BorderFactory.createLineBorder(Color.gray));
    frame.getContentPane().add(toolbar, BorderLayout.NORTH);
    newTabButton = new JButton(newTabAction);
    toolbar.add(newTabButton);
    addButton = new JButton(addAction);
    toolbar.add(addButton);
    cutButton = new JButton(cutAction);
    toolbar.add(cutButton);
    copyButton = new JButton(copyAction);
    toolbar.add(copyButton);
    pasteButton = new JButton(pasteAction);
    toolbar.add(pasteButton);
    moveUpButton = new JButton(moveUpAction);
    toolbar.add(moveUpButton);
    moveDownButton = new JButton(moveDownAction);
    toolbar.add(moveDownButton);
    titleLabel = new JLabel(LaunchRes.getString("Label.Title")); //$NON-NLS-1$
    titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 2));
    toolbar.add(titleLabel);
    toolbar.add(titleField);
    toolbar.add(buttonViewCheckBox);
    hideRootCheckBox = new JCheckBox();
    hideRootCheckBox.addActionListener(changeAction);
    hideRootCheckBox.setContentAreaFilled(false);
    toolbar.add(hideRootCheckBox);
    // create menu items
    int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
    // create new tabset item
    newItem = new JMenuItem(newTabSetAction);
    newItem.setAccelerator(KeyStroke.getKeyStroke('N', mask));
    // create preview item
    previewItem = new JMenuItem();
    previewItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        String base = tabSetBasePath;
        previewing = true;
        LaunchSet set = new LaunchSet(LaunchBuilder.this, tabSetName);
        XMLControlElement control = new XMLControlElement(set);
        control.setValue("filename", tabSetName); //$NON-NLS-1$
        Launcher launcher = new Launcher(control.toXML());
        LaunchNode node = LaunchBuilder.this.getSelectedNode();
        if(node!=null) {
          launcher.setSelectedNode(node.getPathString());
        }
        Point p = LaunchBuilder.this.frame.getLocation();
        launcher.frame.setLocation(p.x+24, p.y+24);
        launcher.frame.setVisible(true);
        // set close operation to dispose to distinguish from default hide
        launcher.frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        tabSetBasePath = base;
        previewing = false;
        launcher.password = password;
        launcher.previewing = true;
        launcher.spawner = LaunchBuilder.this;
        launcher.refreshGUI();
      }

    });
    // create other menu items
    importItem = new JMenuItem(importAction);
    saveJarItem = new JMenuItem(saveJarAction);
    saveNodeItem = new JMenuItem(saveAction);
    saveNodeAsItem = new JMenuItem(saveAsAction);
    saveAllItem = new JMenuItem(saveAllAction);
    openTabItem = new JMenuItem(openTabAction);
    saveAllItem.setAccelerator(KeyStroke.getKeyStroke('S', mask));
    saveSetAsItem = new JMenuItem(saveSetAsAction);
    toolsMenu = new JMenu();
    frame.getJMenuBar().add(toolsMenu, 2);
    encryptionToolItem = new JMenuItem();
    toolsMenu.add(encryptionToolItem);
    encryptionToolItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        EncryptionTool.getTool().setVisible(true);
      }

    });
    // replace tab listener
    tabbedPane.removeMouseListener(tabListener);
    tabListener = new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        if(contentPane.getTopLevelAncestor()!=frame) {
          return;
        }
        if(OSPRuntime.isPopupTrigger(e)) {
          // make popup and add items
          JPopupMenu popup = new JPopupMenu();
          JMenuItem item = new JMenuItem(LaunchRes.getString("MenuItem.Close")); //$NON-NLS-1$
          item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
              removeSelectedTab();
            }

          });
          popup.add(item);
          popup.addSeparator();
          item = new JMenuItem(LaunchRes.getString("Menu.File.SaveAs"));         //$NON-NLS-1$
          item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
              LaunchNode node = getSelectedTab().getRootNode();
              if(saveAs(node)!=null) {
                int i = tabbedPane.getSelectedIndex();
                tabbedPane.setTitleAt(i, node.toString());
              }
              refreshGUI();
            }

          });
          popup.add(item);
          final int i = tabbedPane.getSelectedIndex();
          if((i>0)||(i<tabbedPane.getTabCount()-1)) {
            popup.addSeparator();
          }
          if(i<tabbedPane.getTabCount()-1) {
            item = new JMenuItem(LaunchRes.getString("Popup.MenuItem.MoveUp")); //$NON-NLS-1$
            item.addActionListener(new ActionListener() {
              public void actionPerformed(ActionEvent e) {
                LaunchPanel tab = getSelectedTab();
                LaunchNode root = tab.getRootNode();
                LaunchBuilder.super.removeSelectedTab();
                tabbedPane.insertTab(getDisplayName(root.getFileName()), null, tab, null, i+1);
                tabbedPane.setSelectedComponent(tab);
                tabs.add(i+1, tab);
              }

            });
            popup.add(item);
          }
          if(i>0) {
            item = new JMenuItem(LaunchRes.getString("Popup.MenuItem.MoveDown")); //$NON-NLS-1$
            item.addActionListener(new ActionListener() {
              public void actionPerformed(ActionEvent e) {
                LaunchPanel tab = getSelectedTab();
                LaunchNode root = tab.getRootNode();
                LaunchBuilder.super.removeSelectedTab();
                tabbedPane.insertTab(getDisplayName(root.getFileName()), null, tab, null, i-1);
                tabbedPane.setSelectedComponent(tab);
                tabs.add(i-1, tab);
              }

            });
            popup.add(item);
          }
          popup.show(tabbedPane, e.getX(), e.getY()+8);
        }
      }

    };
    tabbedPane.addMouseListener(tabListener);
    frame.pack();
    displaySplitPane.setDividerLocation(0.7);
  }

  /**
   * Sets the font level.
   *
   * @param level the level
   */
  protected void setFontLevel(int level) {
    final int prev = displaySplitPane.getLastDividerLocation();
    final String divider = displaySplitPane.getName();
    // set font levels of titled borders
    FontSizer.setFonts(displayTitle, level);
    FontSizer.setFonts(commentTitle, level);
    FontSizer.setFonts(descriptionTitle, level);
    FontSizer.setFonts(optionsTitle, level);
    FontSizer.setFonts(securityTitle, level);
    super.setFontLevel(level);
    if(divider!=null) {
      Runnable runner = new Runnable() {
        public void run() {
          double loc = Double.parseDouble(divider);
          loc = Math.max(0.0, loc);
          loc = Math.min(1.0, loc);
          displaySplitPane.setDividerLocation(loc);
          displaySplitPane.setLastDividerLocation(prev);
        }

      };
      SwingUtilities.invokeLater(runner);
    }
  }

  /**
   * Creates the actions.
   */
  protected void createActions() {
    String imageFile = "/org/opensourcephysics/resources/tools/images/open.gif"; //$NON-NLS-1$
    openIcon = loadIcon(imageFile);
    openJarAction = new AbstractAction(null, openIcon) {
      public void actionPerformed(ActionEvent e) {
        JFileChooser chooser = getJARChooser();
        int result = chooser.showOpenDialog(null);
        if(result==JFileChooser.APPROVE_OPTION) {
          File file = chooser.getSelectedFile();
          String newJar = XML.getRelativePath(file.getPath());
          String jars = jarField.getText();
          if(jars.indexOf(newJar)>-1) {
            newJar = null;
          }
          if(!jars.equals("")) { //$NON-NLS-1$
            jars += ";";         //$NON-NLS-1$
          }
          if(newJar!=null) {
            jarField.setText(jars+newJar);
          }
          OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
          searchJarAction.setEnabled(true);
          refreshSelectedNode();
        }
      }

    };
    searchJarAction = new AbstractAction(null, openIcon) {
      public void actionPerformed(ActionEvent e) {
        LaunchNode node = getSelectedNode();
        if((node!=null)&&getClassChooser().chooseClassFor(node)) {
          if(node.getOwner()!=null) {
            changedFiles.add(node.getOwner().getFileName());
          } else {
            changedFiles.add(tabSetName);
          }
          refreshClones(node);
          refreshGUI();
        }
      }

    };
    searchJarAction.setEnabled(false);
    saveJarAction = new AbstractAction(LaunchRes.getString("LaunchBuilder.Action.CreateJar.Name")) { //$NON-NLS-1$
      public void actionPerformed(ActionEvent e) {
        // define base folder
        File base = new File(XML.getDirectoryPath(OSPRuntime.getLaunchJarPath()));
        String jarName = XML.stripExtension(LaunchBuilder.this.tabSetName)+".jar";                   //$NON-NLS-1$
        File target = new File(base, jarName);
        // source list includes launch jar plus user-selected files/folders
        final ArrayList<String> source = new ArrayList<String>();
        JarTreeDialog jarChooser = new JarTreeDialog(frame, base);
        // in jarChooser, preselect launch jar and all non-zip/jar child files
        source.add(OSPRuntime.getLaunchJarName());                                                   // launch jar added first
        // add files in accept method
        base.listFiles(new java.io.FileFilter() {
          public boolean accept(File f) {
            // skip jars, zip files and other undesirables
            if(f.getName().endsWith(".jar")||                                                        //$NON-NLS-1$
              f.getName().endsWith(".zip")||                                                         //$NON-NLS-1$
                f.getName().endsWith(".DS_Store")||                                                  //$NON-NLS-1$
                  f.getName().endsWith(".localized")||                                               //$NON-NLS-1$
                    f.getName().endsWith(".tmp")) {                                                  //$NON-NLS-1$
              return false;
            }
            // file is accepted, so add to source list
            source.add(f.getName());
            return true;
          }

        });
        jarChooser.setSelectionRelativePaths(source.toArray(new String[0]));
        jarChooser.setVisible(true);
        String[] paths = jarChooser.getSelectionRelativePaths();
        if(paths==null) {
          return;
        }
        source.clear();
        // add paths to source list
        for(int i = 0; i<paths.length; i++) {
          if(paths[i].equals(OSPRuntime.getLaunchJarName())) {
            // put launch jar first so later paths will overwrite
            source.add(0, paths[i]);
          } else {
            source.add(paths[i]);
          }
        }
        java.util.jar.Manifest manifest = JarTool.createManifest("", "org.opensourcephysics.tools.Launcher"); //$NON-NLS-1$//$NON-NLS-2$
        JarTool.alwaysOverwrite();    // overwrite all with no warning
        JarTool.setOwnerFrame(frame); // set owner of progress dialog
        JarTool.getTool().create(source, base, target, manifest);
      }

    };
    openArgAction = new AbstractAction(null, openIcon) {
      public void actionPerformed(ActionEvent e) {
        JFileChooser chooser = getFileChooser();
        int result = chooser.showOpenDialog(null);
        if(result==JFileChooser.APPROVE_OPTION) {
          File file = chooser.getSelectedFile();
          argField.setText(XML.getRelativePath(file.getPath()));
          OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
          refreshSelectedNode();
        }
      }

    };
    openModelArgAction = new AbstractAction(null, openIcon) {
      public void actionPerformed(ActionEvent e) {
        JFileChooser chooser = getFileChooser();
        int result = chooser.showOpenDialog(null);
        if(result==JFileChooser.APPROVE_OPTION) {
          File file = chooser.getSelectedFile();
          modelArgField.setText(XML.getRelativePath(file.getPath()));
          OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
          refreshSelectedNode();
        }
      }

    };
    openURLAction = new AbstractAction(LaunchRes.getString("Popup.MenuItem.OpenHTML")) { //$NON-NLS-1$
      public void actionPerformed(ActionEvent e) {
        JFileChooser chooser = getHTMLChooser();
        int result = chooser.showOpenDialog(null);
        if(result==JFileChooser.APPROVE_OPTION) {
          File file = chooser.getSelectedFile();
          pathField.setText(XML.getRelativePath(file.getPath()));
          OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
          refreshSelectedNode();
        }
      }

    };
    searchJarForModelAction = new AbstractAction(LaunchRes.getString("Popup.MenuItem.OpenModel")) { //$NON-NLS-1$
		  public void actionPerformed(ActionEvent e) {
		    Class<?> type = getClassChooser().chooseModel(pathField.getText());
		    if(type!=null) {
		      pathField.setText(type.getName());
		      refreshSelectedNode();
		    }
		  }
		};
    searchJarForModelAction.setEnabled(false);
    openTabAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        LaunchNode node = getSelectedNode();
        String tabName = node.toString();
        for(int i = 0; i<tabbedPane.getComponentCount(); i++) {
          if(tabbedPane.getTitleAt(i).equals(tabName)) {
            tabbedPane.setSelectedIndex(i);
            return;
          }
        }
        XMLControl control = new XMLControlElement(node);
        XMLControl cloneControl = new XMLControlElement(control);
        LaunchNode clone = (LaunchNode) cloneControl.loadObject(null);
        clone.setFileName(node.getFileName());
        addTab(clone);
        refreshGUI();
      }

    };
    changeAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        refreshSelectedNode();
      }

    };
    newTabSetAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        String[] prevArgs = undoManager.getLauncherState();
        if(removeAllTabs()) {
          if(prevArgs!=null) {
            // null new args indicate not redoable
            LauncherUndo.LoadEdit edit = undoManager.new LoadEdit(null, prevArgs);
            undoSupport.postEdit(edit);
          }
          LaunchNode root = new LaunchNode(LaunchRes.getString("NewTab.Name")); //$NON-NLS-1$
          addTab(root);
        }
      }

    };
    addAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        LaunchNode newNode = new LaunchNode(LaunchRes.getString("NewNode.Name")); //$NON-NLS-1$
        addChildToSelectedNode(newNode);
      }

    };
    newTabAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        LaunchNode root = new LaunchNode(LaunchRes.getString("NewTab.Name")); //$NON-NLS-1$
        addTab(root);
      }

    };
    cutAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        copyAction.actionPerformed(null);
        removeSelectedNodes();
      }
    };
    copyAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        if(getSelectedTab()!=null) {
          ArrayList<LaunchNode> nodes = getSelectedTab().getSelectedNodes();
          if(nodes!=null) {
          	NodeSet nodeSet = new NodeSet(nodes);
            XMLControl control = new XMLControlElement(nodeSet);
            StringSelection data = new StringSelection(control.toXML());
            Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            clipboard.setContents(data, data);
          }
        }
      }

    };
    pasteAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        try {
          Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
          Transferable data = clipboard.getContents(null);
          String dataString = (String) data.getTransferData(DataFlavor.stringFlavor);
          if(dataString!=null) {
            XMLControlElement control = new XMLControlElement();
            control.readXML(dataString);
            if(control.getObjectClass()==NodeSet.class) {
              NodeSet nodeSet = (NodeSet) control.loadObject(null);
              for (Object[] next: nodeSet.nodes) {
              	LaunchNode node = (LaunchNode)next[0];
              	if (next.length>1)
              		node.setFileName(next[1].toString());
                addChildToSelectedNode(node);              	
              }
            }
          }
        } catch(Exception ex) {
          ex.printStackTrace();
        }
      }

    };
    importAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        getXMLChooser().setFileFilter(xmlFileFilter);
        int result = getXMLChooser().showOpenDialog(null);
        if(result==JFileChooser.APPROVE_OPTION) {
          // open the file in an xml control
          File file = getXMLChooser().getSelectedFile();
          String fileName = file.getAbsolutePath();
          OSPRuntime.chooserDir = XML.getDirectoryPath(file.getPath());
          XMLControlElement control = new XMLControlElement(fileName);
          if(control.failedToRead()) {
            OSPLog.info(LaunchRes.getString("Log.Message.InvalidXML")+" "+fileName);                                         //$NON-NLS-1$//$NON-NLS-2$
            JOptionPane.showMessageDialog(null, LaunchRes.getString("Dialog.InvalidXML.Message")+" \""+XML.getName(fileName) //$NON-NLS-1$ //$NON-NLS-2$
                                          +"\"",                                                                          //$NON-NLS-1$
                                            LaunchRes.getString("Dialog.InvalidXML.Title"), JOptionPane.WARNING_MESSAGE); //$NON-NLS-1$
            return;
          }
          if(control.getObjectClass()==LaunchNode.class) {
            // add the child node
            LaunchNode child = (LaunchNode) control.loadObject(null);
            child.setFileName(fileName);
            addChildToSelectedNode(child);
          } else {
            OSPLog.info(LaunchRes.getString("Log.Message.NotLauncherFile")+" "+fileName);                                 //$NON-NLS-1$//$NON-NLS-2$
            JOptionPane.showMessageDialog(null, LaunchRes.getString("Dialog.NotLauncherFile.Message")+" \""               //$NON-NLS-1$ //$NON-NLS-2$
                                          +XML.getName(fileName)+"\"",                           //$NON-NLS-1$
                                            LaunchRes.getString("Dialog.NotLauncherFile.Title"), //$NON-NLS-1$
                                            JOptionPane.WARNING_MESSAGE);
          }
        }
      }

    };
    saveAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        LaunchNode node = getSelectedNode();
        if(node.getFileName()!=null) {
          save(node, node.getFileName());
          refreshGUI();
        }
      }

    };
    saveAsAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        LaunchNode node = getSelectedNode();
        LaunchNode parent = (LaunchNode) node.getParent();
        String fileName = saveAs(node);
        if(fileName!=null) {
          // set ancestors to self-contained=false so this node will be saved correctly
          selfContained = false;
          Enumeration<?> en = node.pathFromAncestorEnumeration(node.getRoot());
          while(en.hasMoreElements()) {
            LaunchNode next = (LaunchNode) en.nextElement();
            next.setSelfContained(false);
            next.parentSelfContained = false;
          }
          if(parent!=null) {
            if(parent.getOwner()!=null) {
              changedFiles.add(parent.getOwner().getFileName());
            }
            refreshClones(parent);
          }
        }
        refreshGUI();
      }

    };
    saveAllAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        if(tabSetName.equals(LaunchRes.getString("Tabset.Name.New"))) { //$NON-NLS-1$
          saveTabSetAs();
        } else {
          saveTabSet();
        }
        refreshGUI();
      }

    };
    saveSetAsAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        saveTabSetAs();
        refreshGUI();
      }

    };
    moveUpAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
      	ArrayList<LaunchNode> nodes = getSelectedTab().getSelectedNodes();
      	if (nodes == null) return;
      	// move nodes up in top to bottom order
        for (LaunchNode node: nodes) {
	        LaunchNode parent = (LaunchNode) node.getParent();
	        if(parent==null) {
	          continue;
	        }
	        int i = parent.getIndex(node);
	        if(i>0 && !nodes.contains(parent.getChildBefore(node))) {
	          getSelectedTab().treeModel.removeNodeFromParent(node);
	          getSelectedTab().treeModel.insertNodeInto(node, parent, i-1);
	          if(parent.getOwner()!=null) {
	            changedFiles.add(parent.getOwner().getFileName());
	          } else {
	            changedFiles.add(tabSetName);
	          }
	        }
        }
        getSelectedTab().setSelectedNodes(nodes);
        refreshGUI();
      }

    };
    moveDownAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
      	ArrayList<LaunchNode> nodes = getSelectedTab().getSelectedNodes();
      	if (nodes == null) return;
      	// move nodes down in bottom to top order
        for (int j = nodes.size()-1; j > -1; j--) {
        	LaunchNode node = nodes.get(j);
	        LaunchNode parent = (LaunchNode) node.getParent();
	        if(parent==null) {
	          continue;
	        }
	        int i = parent.getIndex(node);
	        int end = parent.getChildCount();
	        if(i<end-1 && !nodes.contains(parent.getChildAfter(node))) {
	          getSelectedTab().treeModel.removeNodeFromParent(node);
	          getSelectedTab().treeModel.insertNodeInto(node, parent, i+1);
	          if(parent.getOwner()!=null) {
	            changedFiles.add(parent.getOwner().getFileName());
	          } else {
	            changedFiles.add(tabSetName);
	          }
	        }
        }
        getSelectedTab().setSelectedNodes(nodes);
        refreshGUI();
      }

    };
    // create focus listener
    focusListener = new FocusAdapter() {
      public void focusLost(FocusEvent e) {
        refreshSelectedNode();
        refreshGUI();
      }

    };
    // create key listener
    keyListener = new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        JComponent comp = (JComponent) e.getSource();
        if((e.getKeyCode()==KeyEvent.VK_ENTER)&&(((comp!=descriptionPane)&&(comp!=commentPane))||e.isControlDown()||e.isShiftDown())) {
          refreshSelectedNode();
          refreshGUI();
        } else {
          comp.setBackground(Color.yellow);
        }
      }

    };
  }

  /**
   * Removes the selected node.
   */
  public void removeSelectedNode() {
    LaunchNode node = getSelectedNode();
    if((node==null)||(node.getParent()==null)) {
      return;
    }
    LaunchNode parent = (LaunchNode) node.getParent();
    getSelectedTab().treeModel.removeNodeFromParent(node);
    getSelectedTab().setSelectedNode(parent);
    if(parent.getOwner()!=null) {
      changedFiles.add(parent.getOwner().getFileName());
    } else {
      changedFiles.add(tabSetName);
    }
    refreshClones(parent);
    refreshGUI();
  }

  /**
   * Removes the selected nodes.
   */
  public void removeSelectedNodes() {
  	if (getSelectedTab()==null) return;
  	ArrayList<LaunchNode> nodes = getSelectedTab().getSelectedNodes();
  	if (nodes == null) return;
  	LaunchNode toSelect = null; // parent of first (top) node removed
    for (LaunchNode node: nodes) {
	    if((node.getParent()==null) || node.getRoot()!=getRootNode()) {
	      continue;
	    }
	    LaunchNode parent = (LaunchNode) node.getParent();
	    if (toSelect == null)
	    	toSelect = parent;
	    getSelectedTab().treeModel.removeNodeFromParent(node);
	    if(parent.getOwner()!=null) {
	      changedFiles.add(parent.getOwner().getFileName());
	    } else {
	      changedFiles.add(tabSetName);
	    }
	    refreshClones(parent);
    }
    if (toSelect != null) {
		  getSelectedTab().setSelectedNode(toSelect);
	    refreshGUI();
    }
  }

  /**
   * Adds a child node to the selected node.
   *
   * @param child the child node to add
   */
  public void addChildToSelectedNode(LaunchNode child) {
    LaunchNode parent = getSelectedNode();
    if((parent!=null)&&(child!=null)) {
      LaunchNode[] nodes = child.getAllOwnedNodes();
      for(int i = 0; i<nodes.length; i++) {
        LaunchNode node = getSelectedTab().getClone(nodes[i]);
        if(node!=null) {
          getSelectedTab().setSelectedNode(node);
          JOptionPane.showMessageDialog(frame, LaunchRes.getString("Dialog.DuplicateNode.Message")+" \""+node+"\"", //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
            LaunchRes.getString("Dialog.DuplicateNode.Title"), //$NON-NLS-1$
              JOptionPane.WARNING_MESSAGE);
          return;
        }
      }
      getSelectedTab().treeModel.insertNodeInto(child, parent, parent.getChildCount());
      getSelectedTab().tree.scrollPathToVisible(new TreePath(child.getPath()));
      child.setLaunchClass(child.launchClassName);
      if(parent.getOwner()!=null) {
        changedFiles.add(parent.getOwner().getFileName());
      } else {
        changedFiles.add(tabSetName);
      }
      refreshClones(parent);
      refreshGUI();
    }
  }

  /**
   * Replaces clones of a specified node with new clones.
   *
   * @param node the current version of the node to clone
   */
  protected void refreshClones(LaunchNode node) {
    Map<LaunchPanel, LaunchNode> clones = getClones(node);
    replaceClones(node, clones);
  }

  /**
   * Replaces nodes with clones of the specified node.
   *
   * @param node the current version of the node to clone
   * @param clones the current clones to replace
   */
  protected void replaceClones(LaunchNode node, Map<LaunchPanel, LaunchNode> clones) {
    // find clones
    if(clones.isEmpty()) {
      return;
    }
    // replace clones
    XMLControl control = new XMLControlElement(node.getOwner());
    Iterator<LaunchPanel> it = clones.keySet().iterator();
    while(it.hasNext()) {
      LaunchPanel tab = it.next();
      LaunchNode clone = clones.get(tab);
      LaunchNode parent = (LaunchNode) clone.getParent();
      boolean expanded = tab.tree.isExpanded(new TreePath(clone.getPath()));
      if(parent!=null) {
        int index = parent.getIndex(clone);
        tab.treeModel.removeNodeFromParent(clone);
        clone = (LaunchNode) new XMLControlElement(control).loadObject(null);
        clone.setFileName(node.getFileName());
        tab.treeModel.insertNodeInto(clone, parent, index);
      } else {
        clone = (LaunchNode) new XMLControlElement(control).loadObject(null);
        clone.setFileName(node.getFileName());
        tab.treeModel.setRoot(clone);
      }
      if(expanded) {
        tab.tree.expandPath(new TreePath(clone.getPath()));
      }
    }
  }

  /**
   * Returns clones containing a specified node in a tab-to-node map.
   *
   * @param node the node
   * @return the tab-to-node map
   */
  protected Map<LaunchPanel, LaunchNode> getClones(LaunchNode node) {
    Map<LaunchPanel, LaunchNode> clones = new HashMap<LaunchPanel, LaunchNode>();
    // find clones
    node = node.getOwner();
    if(node==null) {
      return clones;
    }
    Component[] tabs = tabbedPane.getComponents();
    for(int i = 0; i<tabs.length; i++) {
      LaunchPanel tab = (LaunchPanel) tabs[i];
      LaunchNode clone = tab.getClone(node);
      if((clone!=null)&&(clone!=node)) {
        clones.put(tab, clone);
      }
    }
    return clones;
  }

  /**
   * Gets a file chooser for selecting jar files.
   *
   * @return the jar chooser
   */
  protected static JFileChooser getJARChooser() {
    getFileChooser().setFileFilter(jarFileFilter);
    return fileChooser;
  }

  /**
   * Gets a file chooser for selecting html files.
   *
   * @return the html chooser
   */
  public static JFileChooser getHTMLChooser() {
    getFileChooser().setFileFilter(htmlFileFilter);
    return fileChooser;
  }

  /**
   * Gets a file chooser.
   *
   * @return the file chooser
   */
  protected static JFileChooser getFileChooser() {
    if(fileChooser==null) {
      fileChooser = new JFileChooser(new File(OSPRuntime.chooserDir));
      allFileFilter = fileChooser.getFileFilter();
      // create jar file filter
      jarFileFilter = new javax.swing.filechooser.FileFilter() {
        // accept all directories and *.jar files.
        public boolean accept(File f) {
          if(f==null) {
            return false;
          }
          if(f.isDirectory()) {
            return true;
          }
          String extension = null;
          String name = f.getName();
          int i = name.lastIndexOf('.');
          if((i>0)&&(i<name.length()-1)) {
            extension = name.substring(i+1).toLowerCase();
          }
          if((extension!=null)&&extension.equals("jar")) {                 //$NON-NLS-1$
            return true;
          }
          return false;
        }
        // the description of this filter
        public String getDescription() {
          return LaunchRes.getString("FileChooser.JarFilter.Description"); //$NON-NLS-1$
        }

      };
      // create html file filter
      htmlFileFilter = new javax.swing.filechooser.FileFilter() {
        // accept all directories, *.htm and *.html files.
        public boolean accept(File f) {
          if(f==null) {
            return false;
          }
          if(f.isDirectory()) {
            return true;
          }
          String extension = null;
          String name = f.getName();
          int i = name.lastIndexOf('.');
          if((i>0)&&(i<name.length()-1)) {
            extension = name.substring(i+1).toLowerCase();
          }
          if((extension!=null)&&(extension.equals("htm")||extension.equals("html"))) { //$NON-NLS-1$//$NON-NLS-2$
            return true;
          }
          return false;
        }
        // the description of this filter
        public String getDescription() {
          return LaunchRes.getString("FileChooser.HTMLFilter.Description"); //$NON-NLS-1$
        }

      };
    }
    fileChooser.removeChoosableFileFilter(jarFileFilter);
    fileChooser.removeChoosableFileFilter(htmlFileFilter);
    fileChooser.setFileFilter(allFileFilter);
    return fileChooser;
  }

  /**
   * Handles a mouse pressed event.
   *
   * @param e the mouse event
   * @param tab the launch panel triggering the event
   */
  protected void handleMousePressed(MouseEvent e, final LaunchPanel tab) {
    super.handleMousePressed(e, tab);
    if(OSPRuntime.isPopupTrigger(e)) {
      TreePath path = tab.tree.getPathForLocation(e.getX(), e.getY());
      if(path==null) {
        return;
      }
      final LaunchNode node = getSelectedNode();
      if(node==null) {
        return;
      }
      // add items to popup
      String fileName = node.getFileName();
      if((fileName!=null)&&changedFiles.contains(fileName)) {
        // add saveNode item
        if(popup.getComponentCount()!=0) {
          popup.addSeparator();
        }
        popup.add(saveNodeItem);
      }
      // add saveNodeAs item
      if(popup.getComponentCount()!=0) {
        popup.addSeparator();
      }
      popup.add(saveNodeAsItem);
      // add openTab item
      if(!node.isRoot()) {
        popup.addSeparator();
        openTabItem.setText(LaunchRes.getString("Action.OpenTab")); //$NON-NLS-1$
        popup.add(openTabItem);
      }
      popup.show(tab, e.getX()+4, e.getY()+12);
    }
  }

  /**
   * Overrides Launcher exit method.
   */
  protected void exit() {
    OSPRuntime.setAuthorMode(false);
    if(!saveAllChanges()) {
      // change default close operation to prevent window closing
      final int op = frame.getDefaultCloseOperation();
      frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
      // restore default close operation
      Runnable runner = new Runnable() {
        public void run() {
          frame.setDefaultCloseOperation(op);
        }

      };
      SwingUtilities.invokeLater(runner);
      return;
    }
    super.exit();
  }

  /**
   * Returns true if tabset is writable.
   *
   * @return true if writable
   */
  protected boolean isTabSetWritable() {
    String path = XML.getResolvedPath(tabSetName, tabSetBasePath);
    Resource res = ResourceLoader.getResource(path);
    File file = (res==null) ? null : res.getFile();
    boolean writable = (file==null) ? true : file.canWrite();
    if(!selfContained) {
      for(int i = 0; i<tabbedPane.getTabCount(); i++) {
        LaunchNode root = getTab(i).getRootNode();
        writable = writable&&isNodeWritable(root);
      }
    }
    return writable;
  }

  /**
   * Returns true if node is writable.
   *
   * @param node the node
   * @return true if writable
   */
  protected boolean isNodeWritable(LaunchNode node) {
    File file = node.getFile();
    boolean writable = (file==null) ? true : file.canWrite();
    // check node's owned child nodes if not self contained
    if(!node.isSelfContained()) {
      LaunchNode[] nodes = node.getChildOwnedNodes();
      for(int i = 0; i<nodes.length; i++) {
        writable = writable&&isNodeWritable(nodes[i]);
      }
    }
    return writable;
  }
  
  protected static class NodeSet {
  	ArrayList<Object[]> nodes;

  	private NodeSet(ArrayList<LaunchNode> list) {
  		nodes = new ArrayList<Object[]>();
  		for (LaunchNode next: list) {
  			Object[] data = new Object[] {next, next.getFileName()};
  			nodes.add(data);
  		}
  	}
  	
  	private NodeSet(XMLControl control) {
  		nodes = new ArrayList<Object[]>();
  		ArrayList<?> input  = (ArrayList<?>)control.getObject("nodes"); //$NON-NLS-1$
  		if (input != null) {
  			for (Object next: input) {
  				nodes.add((Object[])next);
  			}
  		}
  	}
  	
    /**
     * Returns an ObjectLoader to save and load data for this class.
     *
     * @return the object loader
     */
    public static XML.ObjectLoader getLoader() {
      return new Loader();
    }

    /**
     * A class to save and load data for this class.
     */
    static class Loader implements XML.ObjectLoader {

      /**
       * Saves an object's data to an XMLControl.
       *
       * @param control the control to save to
       * @param obj the object to save
       */
      public void saveObject(XMLControl control, Object obj) {
      	NodeSet nodeSet = (NodeSet)obj;
      	control.setValue("nodes", nodeSet.nodes); //$NON-NLS-1$
      }

      /**
       * Creates a new object.
       *
       * @param control the XMLControl with the object data
       * @return the newly created object
       */
      public Object createObject(XMLControl control) {
        return new NodeSet(control);
      }

      /**
       * Loads an object with data from an XMLControl.
       *
       * @param control the control
       * @param obj the object
       * @return the loaded object
       */
      public Object loadObject(XMLControl control, Object obj) {
        return obj;
      }
    }

  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.
 *
 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */

