/* Copyright (C) 1999-2004 by Peter Eastman

   This program is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 2 of the License, or (at your option) any later version.

   This program 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. */

package artofillusion;

import artofillusion.animation.*;
import artofillusion.animation.distortion.*;
import artofillusion.image.*;
import artofillusion.material.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.script.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import bsh.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.swing.*;

/** The LayoutWindow class represents the main window for creating and laying out scenes. */

public class LayoutWindow extends BFrame implements EditingWindow, PopupMenuManager, MenuItemFactory
{
  SceneViewer theView[];
 
  BorderContainer viewPanel[];
  GridContainer viewsContainer;
  FormContainer centerContainer;
  BSplitPane div1, div2;
  BScrollPane itemTreeScroller;
  Score theScore;
  ToolPalette tools;
  EditingTool defaultTool, currentTool;
  BLabel helpText, timeFrameLabel;
  // Panel toolsPanel; (MB) ???
  TreeList itemTree;
  Scene theScene;
  BMenuBar menubar;
  BPopupMenu popupMenu;
  BMenu recentFilesMenu;  // TODO(MB): Remove (change RecentFiles)
  UndoStack undoStack;
  int numViewsShown, currentView;
  boolean modified;

  /** Create a new LayoutWindow for editing a Scene.  Usually, you will not use this constructor directly.
      Instead, call ModellingApp.newWindow(Scene s). */

  public LayoutWindow(Scene s)
  {
    super(s.getName() == null ? "Untitled" : s.getName());
    theScene = s;
    helpText = new BLabel();
    theScore = new Score(this);
    undoStack = new UndoStack();
    createItemList();

    // Create the four SceneViewer panels.

    theView = new SceneViewer [4];
    viewPanel = new BorderContainer [4];
    RowContainer row;
    Object listen = new Object() {
      void processEvent(MousePressedEvent ev)
      {
        for (int i = 0; i < theView.length; i++)
          if (currentView != i && ev.getWidget() == theView[i])
          {
            theView[currentView].setDrawFocus(false);
            theView[i].setDrawFocus(true);
              currentView = i;
              updateMenus();
            updateImage();
          }
      }
    };
    for (int i = 0; i < 4; i++)
    {
      viewPanel[i] = new BorderContainer();
      viewPanel[i].add(row = new RowContainer(), BorderContainer.NORTH);
      viewPanel[i].add(theView[i] = new SceneViewer(theScene, row, this), BorderContainer.CENTER);
      theView[i].setGrid(theScene.getGridSpacing(), theScene.getGridSubdivisions(), theScene.getShowGrid(), theScene.getSnapToGrid());
      theView[i].addEventLink(MousePressedEvent.class, listen);
    }
    theView[1].selectOrientation(2);
    theView[2].selectOrientation(4);
    theView[3].selectOrientation(6);
    theView[3].setPerspective(true);
    theView[currentView].setDrawFocus(true);
    viewsContainer = new GridContainer(1, 1);
    viewsContainer.setDefaultLayout(new LayoutInfo(LayoutInfo.CENTER, LayoutInfo.BOTH, null, null));
    div1 = new BSplitPane(BSplitPane.HORIZONTAL, viewsContainer, itemTreeScroller);
    div1.setResizeWeight(1.0);
    div1.setOneTouchExpandable(true);
    centerContainer = new FormContainer(new double [] {0.0, 1.0}, new double [] {0.0, 1.0, 0.0, 0.0});
    centerContainer.setDefaultLayout(new LayoutInfo(LayoutInfo.CENTER, LayoutInfo.BOTH, null, null));
    centerContainer.add(div1, 1, 0, 1, 3);
    centerContainer.add(helpText, 0, 3, 2, 1);
    div2 = new BSplitPane(BSplitPane.VERTICAL, centerContainer, theScore);
    div2.setResizeWeight(0.75);
    div2.setOneTouchExpandable(true);
    setContent(div2);
    numViewsShown = 1;

    // Build the tool palette.

    tools = new ToolPalette(2, 7);
    EditingTool metaTool, altTool;
    tools.addTool(defaultTool = new MoveObjectTool(this));
    tools.addTool(new RotateObjectTool(this));
    tools.addTool(new ScaleObjectTool(this));
    tools.addTool(new CreateCubeTool(this));
    tools.addTool(new CreateSphereTool(this));
    tools.addTool(new CreateCylinderTool(this));
    tools.addTool(new CreateSplineMeshTool(this));
    tools.addTool(new CreatePolygonTool(this));
    tools.addTool(new CreateCameraTool(this));
    tools.addTool(new CreateLightTool(this));
    tools.addTool(new CreateCurveTool(this, Curve.INTERPOLATING));
    tools.addTool(new CreateCurveTool(this, Curve.APPROXIMATING));
    tools.addTool(metaTool = new MoveViewTool(this));
    tools.addTool(altTool = new RotateSceneViewTool(this, true));
    tools.selectTool(defaultTool);
    for (int i = 0; i < theView.length; i++)
    {
      theView[i].setMetaTool(metaTool);
      theView[i].setAltTool(altTool);
    }

    // Fill in the left hand panel.

    centerContainer.add(tools, 0, 0);
    centerContainer.add(timeFrameLabel = new BLabel(Translate.text("timeFrameLabel", "0.0", "0"), BLabel.CENTER), 0, 2, new LayoutInfo(LayoutInfo.CENTER, LayoutInfo.BOTH, null, null));

    // Build the menubar.
    rebuildMenuBar();

//    menubar = new MenuBar();
//    setMenuBar(menubar);
//    createFileMenu();
//    createEditMenu();
//    createObjectMenu();
//    createToolsMenu();
//    createAnimationMenu();
//    createSceneMenu();
    createPopupMenu();

    toggleViewsCommand();
    UIUtilities.recursivelyAddKeyPressedListeners(this);
    addEventLink(WindowActivatedEvent.class, this, "updateMenus");
    addEventLink(WindowClosingEvent.class, new Object() {
      void processEvent()
      {
        ModellingApp.closeWindow(LayoutWindow.this);
      }
    });
    itemTree.setPopupMenuManager(this);
    UIUtilities.applyDefaultFont(div2);
    UIUtilities.applyDefaultBackground(div2);
    itemTreeScroller.setBackground(Color.white);
    if (ModellingApp.APP_ICON != null)
      ((Frame) getComponent()).setIconImage(ModellingApp.APP_ICON);
    Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
    Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(getComponent().getGraphicsConfiguration());
    setBounds(new Rectangle(screenInsets.left, screenInsets.top, screenDim.width-screenInsets.left-screenInsets.right, screenDim.height-screenInsets.top-screenInsets.bottom));
    div1.addEventLink(ValueChangedEvent.class, this, "updateMenus");
    div2.addEventLink(ValueChangedEvent.class, this, "updateMenus");
    tools.requestFocus();
    div2.setDividerLocation(1.0);
  }

  /** Loads the (default or cutomized) menu structure, builds and sets
    the menubar. */

  public void rebuildMenuBar()
  {
    MenuStructure ms = new MenuStructure();
    ms.loadStructure("MainWindow", false);
    menubar = ms.createMenuBar(this);
    setMenuBar(menubar);
    getComponent().invalidate();
    getComponent().validate();
  }


  /** Create the TreeList containing all the objects in the scene. */

  private void createItemList()
  {
    itemTree = new TreeList(this);
    itemTree.setPreferredSize(new Dimension(130, 100));
    itemTree.addEventLink(TreeList.ElementMovedEvent.class, theScore, "rebuildList");
    itemTree.addEventLink(TreeList.ElementDoubleClickedEvent.class, this, "editObjectCommand");
    System.out.println("LayoutWindow.createItemList itemTree events");
    itemTree.setUpdateEnabled(false);
    for (int i = 0; i < theScene.getNumObjects(); i++)
      {
	ObjectInfo info = theScene.getObject(i);
	if (info.parent == null)
	  itemTree.addElement(new ObjectTreeElement(info, itemTree));
      }
    itemTree.setUpdateEnabled(true);
    itemTreeScroller = new BScrollPane(itemTree) {
      public Dimension getMinimumSize()
      {
        return new Dimension(0, 0);
      }
    };
    itemTreeScroller.setForceWidth(true);
    itemTreeScroller.setForceHeight(true);
    itemTreeScroller.getVerticalScrollBar().setUnitIncrement(10);
    itemTree.addEventLink(SelectionChangedEvent.class, this, "treeSelectionChanged");
    new AutoScroller(itemTreeScroller, 0, 10);
  }

  /** Rebuild the TreeList of objects, attempting as much as possible to preserve its
      current state. */

  public void rebuildItemList()
  {
    boolean expanded[] = new boolean [theScene.getNumObjects()], selected[] = new boolean [theScene.getNumObjects()];

    for (int i = 0; i < theScene.getNumObjects(); i++)
      {
	ObjectInfo info = theScene.getObject(i);
	TreeElement el = itemTree.findElement(info);
	if (el == null)
	  continue;
	expanded[i] = el.isExpanded();
	selected[i] = el.isSelected();
      }
    itemTree.setUpdateEnabled(false);
    itemTree.removeAllElements();
    for (int i = 0; i < theScene.getNumObjects(); i++)
      {
	ObjectInfo info = theScene.getObject(i);
	if (info.parent == null)
	  itemTree.addElement(new ObjectTreeElement(info, itemTree));
      }
    for (int i = 0; i < theScene.getNumObjects(); i++)
      {
	ObjectInfo info = theScene.getObject(i);
	TreeElement el = itemTree.findElement(info);
	if (el == null)
	  continue;
	el.setExpanded(expanded[i]);
	el.setSelected(selected[i]);
      }
    itemTree.setUpdateEnabled(true);
    theScore.rebuildList();
  }

  /** Create the list of tool scripts in the "All Tool Scripts" menu.*/

  public BMenu createScriptsMenu()
  {
    BMenu scriptMenu = Translate.menu("allToolScripts");
    addScriptsToMenu(scriptMenu, new File(ModellingApp.TOOL_SCRIPT_DIRECTORY));
    return scriptMenu;
  }


  /** Create the list of tools in the "All Tools" menu. */

  public BMenu createToolsMenu()
  {
    BMenu toolsMenu = Translate.menu("allTools");
    BMenuItem item;
    ModellingTool tools[] = ModellingApp.getModellingTools();

    for (int i = 0; i < tools.length; i++)
    {
      toolsMenu.add(item = new BMenuItem(tools[i].getName()));
      item.setActionCommand("modellingTool/"+tools[i].getClass().getName());
      item.setName("modellingTool/"+tools[i].getClass().getName());
      item.addEventLink(CommandEvent.class, this, "actionPerformed");
    }
    return toolsMenu;
  }

  public BMenu createImportMenu()
  {
    BMenu importMenu = Translate.menu("import");
    BMenuItem item;
    Translator trans[] = ModellingApp.getTranslators();

    for (int i = 0; i < trans.length; i++)
      if (trans[i].canImport())
      {
        importMenu.add(item = new BMenuItem(trans[i].getName()));
        item.setActionCommand("import/"+trans[i].getClass().getName());
        item.setName("import/"+trans[i].getClass().getName());
        item.addEventLink(CommandEvent.class, this, "actionPerformed");
      }
    return importMenu;
  }

  public BMenu createExportMenu()
  {
    BMenu exportMenu = Translate.menu("export");
    BMenuItem item;
    Translator trans[] = ModellingApp.getTranslators();

    for (int i = 0; i < trans.length; i++)
      if (trans[i].canExport())
      {
        exportMenu.add(item = new BMenuItem(trans[i].getName()));
        item.setActionCommand("export/"+trans[i].getClass().getName());
        item.setName("export/"+trans[i].getClass().getName());
        item.addEventLink(CommandEvent.class, this, "actionPerformed");
      }
    return exportMenu;
  }

  private void addScriptsToMenu(BMenu menu, File dir)
  {
    String files[] = dir.list();
    if (files == null)
      return;
    for (int i = 0; i < files.length; i++)
    {
      File f = new File(dir, files[i]);
      if (f.isDirectory())
      {
        BMenu m = new BMenu(files[i]);
        menu.add(m);
        addScriptsToMenu(m, f);
      }
      else if (files[i].endsWith(".bsh") && files[i].length() > 4)
      {
        BMenuItem item = new BMenuItem(files[i].substring(0, files[i].length()-4));
        item.setActionCommand("beanshell/"+f.getAbsolutePath());
        item.setName("beanshell/"+f.getAbsolutePath());
        item.addEventLink(CommandEvent.class, this, "executeScriptCommand");

        menu.add(item);
      }
    }
  }
  
  /** Returns the default label for a menu item. */
  
  public static String getDefaultMenuLabel(String internalname)
  {
    if (internalname.startsWith("modellingTool/"))
    {
      String classname = internalname.substring(14);

      ModellingTool tools[] = ModellingApp.getModellingTools();

      for (int i = 0; i < tools.length; i++)
      {
        if (tools[i].getClass().getName().equals(classname))
          return tools[i].getName();
      }
    }
    else if (internalname.startsWith("import/"))
    {
      String classname = internalname.substring(7);

      Translator trans[] = ModellingApp.getTranslators();

      for (int i = 0; i < trans.length; i++)
      {
        if (trans[i].canImport() &&
            trans[i].getClass().getName().equals(classname))
          return trans[i].getName();
      }
    }
    else if (internalname.startsWith("export/"))
    {
      String classname = internalname.substring(7);

      Translator trans[] = ModellingApp.getTranslators();


      for (int i = 0; i < trans.length; i++)
      {
        if (trans[i].canExport() &&
            trans[i].getClass().getName().equals(classname))
          return trans[i].getName();
      }
      return classname+"???";
    }
    else      
      return Translate.text("menu."+internalname);

    return "";   //???
  }
  
  
  final String[] MENUITEMS_CHECKBOXITEMS=new String[]
  {"wireframeDisplay", "shadedDisplay", "smoothDisplay", "texturedDisplay",
   "liveRenderSwitch", "showCoordinateAxes"};

  /** Creates a MenuItem which is either a normal MenuItem,
    a CheckboxMenuItem or a whole submenu, depending on
    {@link #isMenuItemCheckbox}.
    An appropriate listener is registered for this automatically.

    @param name Internal name of the item (as recognized by action listener)
    @param label Label of the item (or null for default label)
    @param shortcut Shortcut of the item (or null for default)

    @return The menu item or null if creation is not allowed.
  */

  public MenuWidget createMenuItem(MenuDescription menudesc)
  {
    MenuWidget result = null;
    String name = menudesc.internalName;

    if (name.equals("allToolScripts"))
      result = createScriptsMenu();
    else if (name.equals("allTools"))
      result = createToolsMenu();
    else if (name.equals("import"))
      result = createImportMenu();
    else if (name.equals("export"))
      result = createExportMenu();
    else if (name.equals("openRecent"))
    {
      recentFilesMenu = new BMenu();
      RecentFiles.createMenu(recentFilesMenu);    
      result = recentFilesMenu;
    }
    else if (name.startsWith("modellingTool/") ||
             name.startsWith("import/") ||
             name.startsWith("export/") )
    {
      result = new BMenuItem(getDefaultMenuLabel(name));
      ((Widget)result).addEventLink(CommandEvent.class, this, "actionPerformed");
      ((BMenuItem)result).setActionCommand(name);
      ((BMenuItem)result).setName(name);
    }
    else if (Utilities.findIndexEqual(MENUITEMS_CHECKBOXITEMS, name) > -1)
      result = Translate.checkboxMenuItem(name, this, "itemStateChanged", false);
    else
      result = Translate.menuItem(name, this, "actionPerformed");
    
    menudesc.applyTo(result);

//    if (label != null)
//      result.setLabel(label);
//
//    if (shortcut != null)
//      result.setShortcut(shortcut);

    return result;
  }

  /** Create the popup menu. */

  private void createPopupMenu()
  {
    popupMenu = new BPopupMenu();
    popupMenu.add(Translate.menuItem("editObject", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("objectLayout", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("setTexture", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("setMaterial", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("renameObject", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("convertToTriangle", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("selectChildren", this, "actionPerformed", null));
    popupMenu.addSeparator();
    popupMenu.add(Translate.menuItem("hideSelection", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("showSelection", this, "actionPerformed", null));
    popupMenu.addSeparator();
    popupMenu.add(Translate.menuItem("cut", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("copy", this, "actionPerformed", null));
    popupMenu.add(Translate.menuItem("clear", this, "actionPerformed", null));
  }
//  private void createPopupMenu()
//  {
//    popupMenu = new PopupMenu();
//    popupMenuItem = new MenuItem [12];
//    popupMenu.add(popupMenuItem[0] = Translate.menuItem("editObject", this, null));
//    popupMenu.add(popupMenuItem[1] = Translate.menuItem("objectLayout", this, null));
//    popupMenu.add(popupMenuItem[2] = Translate.menuItem("setTexture", this, null));
//    popupMenu.add(popupMenuItem[3] = Translate.menuItem("setMaterial", this, null));
//    popupMenu.add(popupMenuItem[4] = Translate.menuItem("renameObject", this, null));
//    popupMenu.add(popupMenuItem[5] = Translate.menuItem("convertToTriangle", this, null));
//    popupMenu.add(popupMenuItem[6] = Translate.menuItem("selectChildren", this, null));
//    popupMenu.addSeparator();
//    popupMenu.add(popupMenuItem[7] = Translate.menuItem("hideSelection", this, null));
//    popupMenu.add(popupMenuItem[8] = Translate.menuItem("showSelection", this, null));
//    popupMenu.addSeparator();
//    popupMenu.add(popupMenuItem[9] = Translate.menuItem("cut", this, null));
//    popupMenu.add(popupMenuItem[10] = Translate.menuItem("copy", this, null));
//    popupMenu.add(popupMenuItem[11] = Translate.menuItem("clear", this, null));
//    add(popupMenu);
//  }

  /** Display the popup menu. */

  public void showPopupMenu(Widget w, int x, int y)
  {
    updateMenus();
    popupMenu.show(w, x, y);
  }

  /** Set the wait cursor on everything in this window. */
  
  public void setWaitCursor()
  {
//    recursivelySetCursor(this, Cursor.getDefaultCursor(), Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
  }
  
  /** Remove the wait cursor from everything in this window. */
  
  public void clearWaitCursor()
  {
//    recursivelySetCursor(this, Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR), Cursor.getDefaultCursor());
    setCursor(Cursor.getDefaultCursor());
  }
/*  
  private void recursivelySetCursor(Component c, Cursor oldcursor, Cursor newcursor)
  {
    Cursor curs = c.getCursor();
    if (curs == oldcursor)
      c.setCursor(newcursor);
    if (c instanceof Container)
      {
	Container cont = (Container) c;
	for (int i = 0; i < cont.getComponentCount(); i++)
	  recursivelySetCursor(cont.getComponent(i), oldcursor, newcursor);
      }
  }*/

  public Dimension getMinimumSize()
  {
    return new Dimension(100, 100);
  }

  /* EditingWindow methods. */

  /** This method is called to close the window.  If the Scene has been modified, it first
      gives the user a chance to save the Scene, or to cancel.  If the user cancels it, the
      method returns false.  Otherwise, it closes the window and returns true. */

  public boolean confirmClose()
  {
    if (modified)
    {
      String name = theScene.getName();
      if (name == null)
        name = "Untitled";
      BStandardDialog dlg = new BStandardDialog("", Translate.text("checkSaveChanges", name), BStandardDialog.QUESTION);
      String options[] = new String [] {Translate.text("button.save"), Translate.text("button.dontSave"), Translate.text("button.cancel")};
      int choice = dlg.showOptionDialog(this, options, options[0]);
      if (choice == 0)
      {
        saveCommand();
        if (modified)
          return false;
      }
      if (choice == 2)
        return false;
    }
    dispose();
    return true;
  }

  /** Set the selected EditingTool for this window. */

  public void setTool(EditingTool tool)
  {
    for (int i = 0; i < theView.length; i++)
      theView[i].setTool(tool);
    currentTool = tool;
  }

  /** Set the help text displayed at the bottom of the window. */

  public void setHelpText(String text)
  {
    helpText.setText(text);
  }

  /** Get the Frame corresponding to this window.  (Because LayoutWindow is a Frame,
      it simply returns itself.) */

  public BFrame getFrame()
  {
    return this;
  }

  /** Update the images displayed in all of the viewport. */

  public void updateImage()
  {
    if (numViewsShown == 1)
    {
      theView[currentView].copyOrientationFromCamera();
      theView[currentView].updateImage();
      theView[currentView].repaint();
    }
    else
      for (int i = 0; i < numViewsShown; i++)
      {
        theView[i].copyOrientationFromCamera();
        theView[i].updateImage();
        theView[i].repaint();
      }
  }

  /** Needed for the MenuUpdateHelper class. */
  final String[] MENUITEMS_TRACKS = new String[] {
    "xyzOneTrack", "xyzThreeTracks", "proceduralTrack",
    "xyzOneTrack", "xyzThreeTracks", "quaternionTrack",
    "proceduralTrack", "poseTrack", "bendDistortion",
    "customDistortion", "scaleDistortion", "shatterDistortion",
    "twistDistortion", "constraintTrack", "visibilityTrack",
    "textureTrack"
  };

  /** Helper class. On construction, it stores a few values needed to
    decide the state of each menu item when updateMenuItem is called.
  */
  private class MenuUpdateHelper
  {
    Object sel[] = itemTree.getSelectedObjects();
    int numSelObjects = sel.length;
    Track selTrack[] = theScore.getSelectedTracks();
    int numSelTracks = selTrack.length;
    int numSelKeyframes = theScore.getSelectedKeyframes().length;
    boolean canConvert, canSetMaterial, canSetTexture;
    boolean curve, noncurve, enable, disable, hasChildren, hasParent;
    ObjectInfo info;
    Object3D obj;

    public MenuUpdateHelper()
    {
      int i;
      canConvert = canSetMaterial = canSetTexture = (numSelObjects > 0);
      curve = noncurve = enable = disable = hasChildren = hasParent = false;

      for (i = 0; i < numSelObjects; i++)
      {
        info = (ObjectInfo) sel[i];
        obj = info.object;
        if (obj instanceof Curve)
          curve = true;
        else
          noncurve = true;
        if (obj.canConvertToTriangleMesh() == Object3D.CANT_CONVERT)
          canConvert = false;
        if (!obj.canSetTexture())
          canSetTexture = false;
        if (!obj.canSetMaterial())
          canSetMaterial = false;
        if (info.children.length > 0)
          hasChildren = true;
        if (info.parent != null)
          hasParent = true;
      }
      for (i = 0; i < numSelTracks; i++)
      {
        if (selTrack[i].isEnabled())
          disable = true;
        else
          enable = true;
      }
    }

    /** Updates a single menu item
    (setting enabled/disabled, (un)checked ...). */
    protected void updateMenuItem(MenuWidget item)
    {
      String itemname = ((Widget)item).getName();
      BCheckBoxMenuItem cbitem = null;
      BMenuItem mnitem = null;
      if (item instanceof BCheckBoxMenuItem)
        cbitem = (BCheckBoxMenuItem)item;
      else if (item instanceof BMenuItem)
        mnitem = (BMenuItem)item;

      if ("save".equals(itemname))     // File menu
        mnitem.setEnabled(modified);

      else if ("undo".equals(itemname))   // Edit menu
        mnitem.setEnabled(undoStack.canUndo());
      else if ("redo".equals(itemname))   // Edit menu
        mnitem.setEnabled(undoStack.canRedo());
      else if ("cut".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("copy".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("paste".equals(itemname))
        mnitem.setEnabled(ModellingApp.getClipboardSize() > 0);
      else if ("clear".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("selectChildren".equals(itemname))
        mnitem.setEnabled(hasChildren);
      else if ("duplicate".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("sever".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);

      else if ("editObject".equals(itemname))  // Object menu
        mnitem.setEnabled(numSelObjects == 1 && obj.isEditable());
      else if ("objectLayout".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("transformObject".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("alignObjects".equals(itemname))
        mnitem.setEnabled((numSelObjects > 0) && numSelObjects > 0);
      else if ("setTexture".equals(itemname))
        mnitem.setEnabled((numSelObjects > 0) && canSetTexture);
      else if ("setMaterial".equals(itemname))
        mnitem.setEnabled((numSelObjects > 0) && canSetMaterial);
      else if ("renameObject".equals(itemname))
        mnitem.setEnabled(numSelObjects == 1);
      else if ("convertToTriangle".equals(itemname))
        mnitem.setEnabled(canConvert && numSelObjects >= 1);
      else if ("hideSelection".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("showSelection".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);

      else if ("showScore".equals(itemname))  // Animation menu
        mnitem.setText(Translate.text((theScore.getBounds().height == 0) ? "menu.showScore" : "menu.hideScore"));
      else if ("editKeyframe".equals(itemname))
        mnitem.setEnabled(numSelKeyframes == 1);
      else if ("deleteSelectedKeyframes".equals(itemname))
        mnitem.setEnabled(numSelKeyframes > 0);
      else if ("keyframe".equals(itemname))
        mnitem.setEnabled(numSelTracks > 0);
      else if ("keyframeModified".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("editTrack".equals(itemname))
        mnitem.setEnabled(numSelTracks == 1);
      else if ("duplicateTracks".equals(itemname))
        mnitem.setEnabled(numSelTracks > 0);
      else if ("deleteTracks".equals(itemname))
        mnitem.setEnabled(numSelTracks > 0);
      else if ("selectAllTracks".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
      else if ("enableTracks".equals(itemname))
        mnitem.setEnabled(enable);
      else if ("disableTracks".equals(itemname))
        mnitem.setEnabled(disable);
      else if ("pathFromCurve".equals(itemname))
        mnitem.setEnabled(curve && noncurve);
      else if ("bindToParent".equals(itemname))
        mnitem.setEnabled(hasParent);

      else if (Utilities.findIndexEqual(MENUITEMS_TRACKS, itemname) > -1)
        mnitem.setEnabled(numSelObjects > 0); // Track submenu(s)

      else if ("fourViews".equals(itemname))
      {
        if (numViewsShown == 4)
          mnitem.setText(Translate.text("menu.fourViews"));
        else
          mnitem.setText(Translate.text("menu.oneView"));
      }
      else if ("hideObjectList".equals(itemname))
        mnitem.setText(Translate.text((itemTree.getBounds().width == 0) ? "menu.showObjectList" : "menu.hideObjectList"));
      else if ("showTemplate".equals(itemname))
        mnitem.setText(Translate.text(theView[0].getTemplateShown() ? "menu.showTemplate" : "menu.hideTemplate"));

      else if ("wireframeDisplay".equals(itemname))
        cbitem.setState(theView[currentView].getRenderMode() == ViewerCanvas.RENDER_WIREFRAME);
      else if ("shadedDisplay".equals(itemname))
        cbitem.setState(theView[currentView].getRenderMode() == ViewerCanvas.RENDER_FLAT);
      else if ("smoothDisplay".equals(itemname))
        cbitem.setState(theView[currentView].getRenderMode() == ViewerCanvas.RENDER_SMOOTH);
      else if ("texturedDisplay".equals(itemname))
        cbitem.setState(theView[currentView].getRenderMode() == ViewerCanvas.RENDER_TEXTURED);
      else if ("liveRenderSwitch".equals(itemname))
        cbitem.setState(theView[currentView].isLiveRendering());      
      else if ("showTemplate".equals(itemname))
      {
        if (theView[currentView].getTemplateImage() == null)
          mnitem.setEnabled(false);
        else
        {
          mnitem.setEnabled(true);
          boolean wasShown = theView[currentView].getTemplateShown();
          mnitem.setText(Translate.text(wasShown ? "menu.showTemplate" :
            "menu.hideTemplate"));
        }
        mnitem.setEnabled(theView[currentView].getTemplateImage() != null);
      }
      else if ("showCoordinateAxes".equals(itemname))
        cbitem.setState(theView[currentView].getShowAxes());
      else if ("frameSelection".equals(itemname))
        mnitem.setEnabled(numSelObjects > 0);
    }
  }


  public void updateMenus()
  {
    MenuWidget[] mis;
    MenuUpdateHelper helper = new MenuUpdateHelper();

    mis = UIUtilities.getMenuItemList(getMenuBar()); // TODO(MB) Use cache
    for(int i = 0; i < mis.length; ++i)
      helper.updateMenuItem(mis[i]);
    mis = UIUtilities.getMenuItemList(popupMenu);
    for(int i = 0; i < mis.length; ++i)
      helper.updateMenuItem(mis[i]);
  }


  /** Set the UndoRecord which will be executed if the user chooses Undo from the Edit menu. */

  public void setUndoRecord(UndoRecord command)
  {
    undoStack.addRecord(command);
    modified = true;
    updateMenus();
  }

  /** Set whether the scene has been modified since it was last saved. */

  public void setModified()
  {
    modified = true;
  }

  /** Add a new object to the scene.  If undo is not null,
      appropriate commands will be added to it to undo this operation. */

  public void addObject(Object3D obj, CoordinateSystem coords, String name, UndoRecord undo)
  {
    addObject(new ObjectInfo(obj, coords, name), undo);
  }

  /** Add a new object to the scene.  If undo is not null,
      appropriate commands will be added to it to undo this operation. */

  public void addObject(ObjectInfo info, UndoRecord undo)
  {
    theScene.addObject(info, undo);
    itemTree.addElement(new ObjectTreeElement(info, itemTree));
    for (int i = 0; i < theView.length ; i++)
      theView[i].rebuildCameraList();
    theScore.rebuildList();
  }

  /** Add a new object to the scene.  If undo is not null,
      appropriate commands will be added to it to undo this operation. */

  public void addObject(ObjectInfo info, int index, UndoRecord undo)
  {
    theScene.addObject(info, index, undo);
    itemTree.addElement(new ObjectTreeElement(info, itemTree), index);
    for (int i = 0; i < theView.length ; i++)
      theView[i].rebuildCameraList();
    theScore.rebuildList();
  }

  /** Remove an object from the scene.  If undo is not null,
      appropriate commands will be added to it to undo this operation. */

  public void removeObject(int which, UndoRecord undo)
  {
    ObjectInfo info = theScene.getObject(which);
    ObjectInfo parent = info.parent;
    int childIndex = -1;
    if (parent != null)
      for (int i = 0; i < parent.children.length; i++)
        if (parent.children[i] == info)
          childIndex = i;
    itemTree.removeObject(info);
    if (childIndex > -1 && info.parent == null)
      undo.addCommandAtBeginning(UndoRecord.ADD_TO_GROUP, new Object [] {parent, info, new Integer(childIndex)});
    theScene.removeObject(which, undo);
    for (int i = 0; i < theView.length ; i++)
      theView[i].rebuildCameraList();
    theScore.rebuildList();
  }

  /** Set the name of an object in the scene. */

  public void setObjectName(int which, String name)
  {
    ObjectInfo info = theScene.getObject(which);

    setUndoRecord(new UndoRecord(this, false, UndoRecord.RENAME_OBJECT, new Object [] {new Integer(which), info.name}));
    info.name = name;
    itemTree.repaint();
    for (int i = 0; i < theView.length ; i++)
      theView[i].rebuildCameraList();
    theScore.rebuildList();
  }

  /** Set the time which is currently being displayed. */

  public void setTime(double time)
  {
    theScene.setTime(time);
    theScore.setTime(time);
    NumberFormat nf = NumberFormat.getNumberInstance();
    nf.setMaximumFractionDigits(3);
    timeFrameLabel.setText(Translate.text("timeFrameLabel", nf.format(time),
        Integer.toString((int) Math.round(time*theScene.getFramesPerSecond()))));
    theScore.repaint();
    itemTree.repaint();
    updateImage();
  }

  /** Get the Scene associated with this window. */

  public Scene getScene()
  {
    return theScene;
  }

  /** Get the ViewerCanvas which currently has focus. */

  public ViewerCanvas getView()
  {
    return theView[currentView];
  }

  /** Get the Score for this window. */

  public Score getScore()
  {
    return theScore;
  }

  /** Set whether the object list should be displayed. */

  public void setObjectListVisible(boolean visible)
  {
    if (visible)
      div1.resetToPreferredSizes();
    else
      div1.setDividerLocation(1.0);
    div1.layoutChildren();
    updateMenus();
  }

  /** Set whether the score should be displayed. */

  public void setScoreVisible(boolean visible)
  {
    div2.setDividerLocation(visible ? 0.75 : 1.0);
    div2.layoutChildren();
    updateMenus();
  }

  /** Set whether the window is split into four views. */

  public void setSplitView(boolean split)
  {
    if ((numViewsShown == 1) == split)
      toggleViewsCommand();
  }

  /** This is called when the selection in the object tree changes. */

  private void treeSelectionChanged()
  {
    System.out.println("LayoutWindow.treeSelectionChanged");
    Object sel[] = itemTree.getSelectedObjects();
    int which[] = new int [sel.length];

    for (int i = 0; i < sel.length; i++)
      which[i] = theScene.indexOf((ObjectInfo) sel[i]);
    setSelection(which);
    updateImage();
  }
  
  public void itemStateChanged(CommandEvent e)
  {
    Object source = e.getSource();
    int i;
    
    if (source instanceof BCheckBoxMenuItem)
    {
      String command = ((BCheckBoxMenuItem)source).getName();
      
      if ("wireframeDisplay".equals(command))
        theView[currentView].setRenderMode(ViewerCanvas.RENDER_WIREFRAME);
      else if ("shadedDisplay".equals(command))
        theView[currentView].setRenderMode(ViewerCanvas.RENDER_FLAT);
      else if ("smoothDisplay".equals(command))
        theView[currentView].setRenderMode(ViewerCanvas.RENDER_SMOOTH);
      else if ("texturedDisplay".equals(command))
        theView[currentView].setRenderMode(ViewerCanvas.RENDER_TEXTURED);
      else if ("liveRenderSwitch".equals(command))
      {
        BCheckBoxMenuItem cbitem = (BCheckBoxMenuItem)source;
        theView[currentView].setLiveRendering(cbitem.getState());
      }
      else if ("showCoordinateAxes".equals(command))
      {
        BCheckBoxMenuItem cbitem = (BCheckBoxMenuItem)source;
        for(i = 0; i < theView.length; ++i)
          theView[i].setShowAxes(cbitem.getState());
      }

      updateMenus();
      updateImage();
    }
    //    else
    //      {
    //	Object sel[] = itemTree.getSelectedObjects();
    //	int which[] = new int [sel.length];
    //
    //	for (i = 0; i < sel.length; i++)
    //	  which[i] = theScene.indexOf((ObjectInfo) sel[i]);
    //	setSelection(which);
    //	  updateImage();
    //      }
  }

  /* KeyListener methods. */

  private void keyPressed(KeyPressedEvent e)
  {
    int code = e.getKeyCode();

    if (code == KeyPressedEvent.VK_BACK_SPACE || code == KeyPressedEvent.VK_DELETE)
      clearCommand();
    else if (e.isShiftDown() && (code == KeyPressedEvent.VK_LEFT || code == KeyPressedEvent.VK_RIGHT || code == KeyPressedEvent.VK_UP || code == KeyPressedEvent.VK_DOWN))
      tools.keyPressed(e);
    else
      currentTool.keyPressed(e, theView[currentView]);
  }


  /** Set a single object in the scene to be selected. */

  public void setSelection(int which)
  {
    itemTree.setUpdateEnabled(false);
    clearSelection();
    theScene.setSelection(which);
    itemTree.setSelected(theScene.getObject(which), true);
    itemTree.setUpdateEnabled(true);
    theScore.rebuildList();
    updateMenus();
  }

  /** Set the list of objects in the scene which should be selected. */

  public void setSelection(int which[])
  {
    itemTree.setUpdateEnabled(false);
    clearSelection();
    theScene.setSelection(which);
    for (int i = 0; i < which.length; i++)
      itemTree.setSelected(theScene.getObject(which[i]), true);
    itemTree.setUpdateEnabled(true);
    theScore.rebuildList();
    updateMenus();
  }

  /** Set an object to be selected. */

  public void addToSelection(int which)
  {
    theScene.addToSelection(which);
    itemTree.setSelected(theScene.getObject(which), true);
    theScore.rebuildList();
    updateMenus();
  }

  /** Deselect all objects. */

  public void clearSelection()
  {
    theScene.clearSelection();
    itemTree.deselectAll();
    theScore.rebuildList();
    updateMenus();
  }

  /** Deselect a single object. */

  public void removeFromSelection(int which)
  {
    theScene.removeFromSelection(which);
    itemTree.setSelected(theScene.getObject(which), false);
    theScore.rebuildList();
    updateMenus();
  }

  private void actionPerformed(CommandEvent e)
  {
    String command = e.getActionCommand();
    Widget src = e.getWidget();
    Widget menu = (src instanceof MenuWidget ? src.getParent() : null);

    setWaitCursor();
    if (command.equals("new"))
      ModellingApp.newWindow();
    else if (command.equals("open"))
      ModellingApp.openScene(this);
    else if (command.equals("close"))
      ModellingApp.closeWindow(this);
    else if (command.equals("save"))
      saveCommand();
    else if (command.equals("saveas"))
      saveAsCommand();
    else if (command.equals("quit"))
      ModellingApp.quit();
    else if (command.startsWith("import/"))
      importCommand(command.substring(7));
    else if (command.startsWith("export/"))
      exportCommand(command.substring(7));
    else if (command.startsWith("linkExternal"))
      linkExternalCommand();

    else if (command.equals("undo"))
      undoCommand();
    else if (command.equals("cut"))
      cutCommand();
    else if (command.equals("copy"))
      copyCommand();
    else if (command.equals("paste"))
      pasteCommand();
    else if (command.equals("clear"))
      clearCommand();
    else if (command.equals("selectChildren"))
      {
        setSelection(theScene.getSelectionWithChildren());
        updateImage();
      }
    else if (command.equals("selectAll"))
      selectAllCommand();
    else if (command.equals("duplicate"))
      duplicateCommand();
    else if (command.equals("sever"))
      severCommand();
    else if (command.equals("preferences"))
      new PreferencesWindow(this);
    else if (command.equals("editMenu"))
    {
      new MenuEditDialog(this, this);
      rebuildMenuBar();
    }
    else if (command.equals("editObject"))
      editObjectCommand();
    else if (command.equals("objectLayout"))
      objectLayoutCommand();
    else if (command.equals("transformObject"))
      transformObjectCommand();
    else if (command.equals("alignObjects"))
      alignObjectsCommand();
    else if (command.equals("setTexture"))
      setTextureCommand();
    else if (command.equals("setMaterial"))
      setMaterialCommand();
    else if (command.equals("renameObject"))
      renameObjectCommand();
    else if (command.equals("convertToTriangle"))
      convertToTriangleCommand();
    else if (command.equals("convertToActor"))
      convertToActorCommand();
    else if (command.equals("hideSelection"))
      setObjectVisibility(false, true);
    else if (command.equals("showSelection"))
      setObjectVisibility(true, true);
    else if (command.equals("showAll"))
      setObjectVisibility(true, false);

    else if (Utilities.findIndexEqual(CREATABLEOBJECTS, command) > -1)
      createObjectCommand(command);
    else if (command.startsWith("modellingTool/"))
      modellingToolCommand(command.substring(14));
    else if (command.equals("createScriptObject"))
      createScriptObjectCommand();
    else if (command.equals("editScript"))
      new ExecuteScriptWindow(this);
    else if (command.equals("showScore"))
      setScoreVisible(theScore.getBounds().height == 0);
    else if (command.equals("previewAnimation"))
      new AnimationPreviewer(this);
    else if (command.equals("forwardFrame"))
      {
        double t = theScene.getTime() + 1.0/theScene.getFramesPerSecond();
        setTime(t);
      }
    else if (command.equals("backFrame"))
      {
        double t = theScene.getTime() - 1.0/theScene.getFramesPerSecond();
        setTime(t);
      }
    else if (command.equals("jumpToTime"))
      jumpToTimeCommand();
    else if (command.equals("editKeyframe"))
      theScore.editSelectedKeyframe();
    else if (command.equals("deleteSelectedKeyframes"))
      theScore.deleteSelectedKeyframes();
    else if (command.equals("editTrack"))
      theScore.editSelectedTrack();
    else if (command.equals("keyframe"))
      theScore.keyframeSelectedTracks();
    else if (command.equals("keyframeModified"))
      theScore.keyframeModifiedTracks();
    else if (command.equals("duplicateTracks"))
      theScore.duplicateSelectedTracks();
    else if (command.equals("deleteTracks"))
      theScore.deleteSelectedTracks();
    else if (command.equals("selectAllTracks"))
      theScore.selectAllTracks();
    else if (command.equals("enableTracks"))
      theScore.setTracksEnabled(true);
    else if (command.equals("disableTracks"))
      theScore.setTracksEnabled(false);
    else if (command.equals("pathFromCurve"))
      new PathFromCurveDialog(this, itemTree.getSelectedObjects());
    else if (command.equals("bindToParent"))
      bindToParentCommand();
    else if (command.equals("moveKeyframes"))
      new EditKeyframesDialog(this, EditKeyframesDialog.MOVE);
    else if (command.equals("copyKeyframes"))
      new EditKeyframesDialog(this, EditKeyframesDialog.COPY);
    else if (command.equals("rescaleKeyframes"))
      new EditKeyframesDialog(this, EditKeyframesDialog.RESCALE);
    else if (command.equals("loopKeyframes"))
      new EditKeyframesDialog(this, EditKeyframesDialog.LOOP);
    else if (command.equals("deleteKeyframes"))
      new EditKeyframesDialog(this, EditKeyframesDialog.DELETE);
    else if (command.equals("poseTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), PoseTrack.class, null, true);
    else if (command.equals("constraintTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), ConstraintTrack.class, null, true);
    else if (command.equals("visibilityTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), VisibilityTrack.class, null, true);
    else if (command.equals("textureTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), TextureTrack.class, null, true);
    else if (command.equals("xyzOneTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), PositionTrack.class, null, true);
    else if (command.equals("xyzThreeTracks"))
      {
        theScore.addTrack(itemTree.getSelectedObjects(), PositionTrack.class, new Object [] {"Z Position", Boolean.FALSE, Boolean.FALSE, Boolean.TRUE}, true);
        theScore.addTrack(itemTree.getSelectedObjects(), PositionTrack.class, new Object [] {"Y Position", Boolean.FALSE, Boolean.TRUE, Boolean.FALSE}, false);
        theScore.addTrack(itemTree.getSelectedObjects(), PositionTrack.class, new Object [] {"X Position", Boolean.TRUE, Boolean.FALSE, Boolean.FALSE}, false);
      }
    else if (command.equals("proceduralTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), ProceduralPositionTrack.class, null, true);
    else if (command.equals("xyzOneTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), RotationTrack.class, new Object [] {"Rotation", Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE}, true);
    else if (command.equals("xyzThreeTracks"))
      {
        theScore.addTrack(itemTree.getSelectedObjects(), RotationTrack.class, new Object [] {"Z Rotation", Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, Boolean.TRUE}, true);
        theScore.addTrack(itemTree.getSelectedObjects(), RotationTrack.class, new Object [] {"Y Rotation", Boolean.FALSE, Boolean.FALSE, Boolean.TRUE, Boolean.FALSE}, false);
        theScore.addTrack(itemTree.getSelectedObjects(), RotationTrack.class, new Object [] {"X Rotation", Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, Boolean.FALSE}, false);
      }
    else if (command.equals("quaternionTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), RotationTrack.class, new Object [] {"Rotation", Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE}, true);
    else if (command.equals("proceduralTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), ProceduralRotationTrack.class, null, true);
    else if (command.equals("bendDistortion"))
      theScore.addTrack(itemTree.getSelectedObjects(), BendTrack.class, null, true);
    else if (command.equals("customDistortion"))
      theScore.addTrack(itemTree.getSelectedObjects(), CustomDistortionTrack.class, null, true);
    else if (command.equals("IKTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), IKTrack.class, null, true);      
    else if (command.equals("scaleDistortion"))
      theScore.addTrack(itemTree.getSelectedObjects(), ScaleTrack.class, null, true);
    else if (command.equals("shatterDistortion"))
      theScore.addTrack(itemTree.getSelectedObjects(), ShatterTrack.class, null, true);
    else if (command.equals("twistDistortion"))
      theScore.addTrack(itemTree.getSelectedObjects(), TwistTrack.class, null, true);
    else if (command.equals("IKTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), IKTrack.class, null, true);
    else if (command.equals("skeletonShapeTrack"))
      theScore.addTrack(itemTree.getSelectedObjects(), SkeletonShapeTrack.class, null, true);
    else if ("editIterAnimationScript".equals(command))
    {
      getScene().getIterAnimationHolder(0).edit(this);
      setModified();
    }
    else if ("deleteIterAnimationScript".equals(command))
    {
      getScene().removeIterAnimationHolder(0);
      setModified();
    }
    else if ("setIterAnimationInitial".equals(command))
    {
      getScene().getIterAnimationHolder(0).rebuildInitialState(this);
      setModified();
    }
    
    else if (command.equals("renderScene"))
      renderCommand();
    else if (command.equals("renderImmediately"))
      renderImmediatelyCommand();
    else if (command.equals("fourViews"))
      toggleViewsCommand();
    else if (command.equals("hideObjectList"))
      setObjectListVisible(itemTree.getBounds().width == 0);
    else if (command.equals("showTemplate"))
      {
        boolean wasShown = theView[currentView].getTemplateShown();
        theView[currentView].setShowTemplate(!wasShown);
        updateMenus();
        updateImage();
      }
    else if (command.equals("setTemplate"))
      setTemplateCommand();
    else if (command.equals("grid"))
      setGridCommand();
    else if (command.equals("frameSelection"))
      frameWithCameraCommand(true);
    else if (command.equals("frameScene"))
      frameWithCameraCommand(false);
    else if (command.equals("textures"))
      texturesCommand();
    else if (command.equals("materials"))
      materialsCommand();
    else if (command.equals("images"))
      new ImagesDialog(this, theScene, null);
    else if (command.equals("environment"))
      environmentCommand();
    else if (command.equals("setLiveRenderEnvironment"))
      setLiveRenderEnvironmentCommand();
    else if (command.equals("resetLiveRenderEnvironment"))
      resetLiveRenderEnvironmentCommand();
    else if (command.startsWith("beanshell/"))
      executeScript(new File(command.substring(10)));
    clearWaitCursor();
  }

  void importCommand(String transclass)
  {
    Translator trans[] = ModellingApp.getTranslators();

    for (int i = 0; i < trans.length; i++)
      if (trans[i].canImport() &&
          trans[i].getClass().getName().equals(transclass))
	{
	  trans[i].importFile(this);
	  return;
	}
  }

  void exportCommand(String transclass)
  {
    Translator trans[] = ModellingApp.getTranslators();

    for (int i = 0; i < trans.length; i++)
      if (trans[i].canExport() &&
          trans[i].getClass().getName().equals(transclass))
    {
      trans[i].exportFile(this, theScene);
      return;
    }
  }

  void linkExternalCommand()
  {
    BFileChooser fc = new BFileChooser(BFileChooser.OPEN_FILE, Translate.text("externalObject.selectScene"));
    if (!fc.showDialog(this))
      return;
    ExternalObject obj = new ExternalObject(fc.getSelectedFile(), "");
    ObjectInfo info = new ObjectInfo(obj, new CoordinateSystem(), "External Object");
    UndoRecord undo = new UndoRecord(this, false);
    addObject(info, undo);
    setUndoRecord(undo);
    setSelection(theScene.getNumObjects()-1);
    editObjectCommand();
  }
  
  void modellingToolCommand(String toolclass)
  {
    ModellingTool tools[] = ModellingApp.getModellingTools();
    for(int i = 0; i < tools.length; ++i)
      if (tools[i].getClass().getName().equals(toolclass))
        tools[i].commandSelected(this);
  }

  public void saveCommand()
  {
    if (theScene.getName() == null)
      saveAsCommand();
    else
      modified = !ModellingApp.saveScene(theScene, this);
  }
  
  public void saveAsCommand()
  {
    BFileChooser fc = new BFileChooser(BFileChooser.SAVE_FILE, Translate.text("saveScene"));
    if (theScene.getName() == null)
      fc.setSelectedFile(new File("Untitled.aoi"));
    else
      fc.setSelectedFile(new File(theScene.getName()));
    if (theScene.getDirectory() != null)
      fc.setDirectory(new File(theScene.getDirectory()));
    else if (ModellingApp.currentDirectory != null)
      fc.setDirectory(new File(ModellingApp.currentDirectory));
    if (!fc.showDialog(this))
      return;
    theScene.setName(fc.getSelectedFile().getName());
    theScene.setDirectory(fc.getDirectory().getAbsolutePath());
    setTitle(fc.getSelectedFile().getName());
    modified = !ModellingApp.saveScene(theScene, this);
  }

  public void undoCommand()
  {
    undoStack.executeUndo();
    rebuildItemList();
    updateImage();
    updateMenus();
  }

  public void redoCommand()
  {
    undoStack.executeRedo();
    rebuildItemList();
    updateImage();
    updateMenus();
  }
  
  public void cutCommand()
  {
    copyCommand();
    clearCommand();
  }

  public void copyCommand()
  {
    int sel[] = theScene.getSelectionWithChildren();
    if (sel.length == 0)
      return;
    ObjectInfo copy[] = new ObjectInfo [sel.length];
    for (int i = 0; i < sel.length; i++)
      copy[i] = theScene.getObject(sel[i]);
    copy = ObjectInfo.duplicateAll(copy);
    ModellingApp.copyToClipboard(copy, theScene);
    updateMenus();
  }

  public void pasteCommand()
  {
    int which[] = new int [ModellingApp.getClipboardSize()], num = theScene.getNumObjects();
    for (int i = 0; i < which.length; i++)
      which[i] = num+i;
    ModellingApp.pasteClipboard(this);
    setSelection(which);
    rebuildItemList();
    updateImage();
  }

  public void clearCommand()
  {
    Object sel[] = itemTree.getSelectedObjects();
    boolean any;
    int i, j;

    if (sel.length == 0)
      return;
    theScene.clearSelection();
    UndoRecord undo = new UndoRecord(this, false);

    // First remove any selected objects.

    for (i = sel.length-1; i >= 0; i--)
      {
	ObjectInfo info = (ObjectInfo) sel[i];
	int index = theScene.indexOf(info);
	removeObject(index, undo);
      }

    // Now remove any objects whose parents were just deleted.

    do
    {
      any = false;
      for (i = 0; i < theScene.getNumObjects(); i++)
	{
	  ObjectInfo info = theScene.getObject(i);
	  if (info.parent != null && theScene.indexOf(info.parent) == -1)
	    {
	      removeObject(i, undo);
	      i--;
	      any = true;
	    }
	}
    } while (any == true);
    setUndoRecord(undo);
    updateMenus();
    updateImage();
  }

  public void selectAllCommand()
  {
    int i, which[] = new int [theScene.getNumObjects()];

    for (i = 0; i < which.length; i++)
      which[i] = i;
    setSelection(which);
    updateImage();
  }

  public void duplicateCommand()
  {
    Object sel[] = itemTree.getSelectedObjects();
    int i, which[] = new int [sel.length], num = theScene.getNumObjects();

    UndoRecord undo = new UndoRecord(this, false);
    for (i = 0; i < sel.length; i++)
      {
	addObject(((ObjectInfo) sel[i]).duplicate(), undo);
	which[i] = num + i;
      }
    setSelection(which);
    setUndoRecord(undo);
    updateImage();
  }

  public void severCommand()
  {
    Object sel[] = itemTree.getSelectedObjects();
    ObjectInfo info;
    int i;

    UndoRecord undo = new UndoRecord(this, false);
    for (i = 0; i < sel.length; i++)
      {
	info = (ObjectInfo) sel[i];
	undo.addCommand(UndoRecord.COPY_OBJECT_INFO, new Object [] {info, info.duplicate()});
	info.object = info.object.duplicate();
      }
    setUndoRecord(undo);
  }

  public void editObjectCommand()
  {
    System.out.println("LayoutWindow.editObjectCommand");
    int sel[] = theScene.getSelection();
    final Object3D obj;

    if (sel.length != 1)
      return;
    obj = theScene.getObject(sel[0]).object;
    if (obj.isEditable())
      {
        final UndoRecord undo = new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {obj, obj.duplicate()});
	obj.edit(this, theScene.getObject(sel[0]), new Runnable() {     // TODO(MB) Remove the callback some day
	  public void run()
	  {
            setUndoRecord(undo);
	    theScene.objectModified(obj);
	    updateImage();
	    updateMenus();
	  }
	} );
      }
  }

  public void objectLayoutCommand()
  {
    int i, sel[] = theScene.getSelection();
    TransformDialog dlg;
    ObjectInfo obj[] = new ObjectInfo [sel.length];
    Vec3 orig, size;
    double angles[], values[];

    if (sel.length == 0)
      return;
    UndoRecord undo = new UndoRecord(this, false);
    for (i = 0; i < sel.length; i++)
      {
	obj[i] = theScene.getObject(sel[i]);
	undo.addCommand(UndoRecord.COPY_OBJECT, new Object [] {obj[i].object, obj[i].object.duplicate()});
	undo.addCommand(UndoRecord.COPY_COORDS, new Object [] {obj[i].coords, obj[i].coords.duplicate()});
      }
    if (sel.length == 1)
    {
      orig = obj[0].coords.getOrigin();
      angles = obj[0].coords.getRotationAngles();
      size = obj[0].object.getBounds().getSize();
      dlg = new TransformDialog(this, Translate.text("objectLayoutTitle", theScene.getObject(sel[0]).name), 
          new double [] {orig.x, orig.y, orig.z, angles[0], angles[1], angles[2], 
          size.x, size.y, size.z}, false, false);
      if (!dlg.clickedOk())
        return;
      values = dlg.getValues();
      if (!Double.isNaN(values[0]))
        orig.x = values[0];
      if (!Double.isNaN(values[1]))
        orig.y = values[1];
      if (!Double.isNaN(values[2]))
        orig.z = values[2];
      if (!Double.isNaN(values[3]))
        angles[0] = values[3];
      if (!Double.isNaN(values[4]))
        angles[1] = values[4];
      if (!Double.isNaN(values[5]))
        angles[2] = values[5];
      if (!Double.isNaN(values[6]))
        size.x = values[6];
      if (!Double.isNaN(values[7]))
        size.y = values[7];
      if (!Double.isNaN(values[8]))
        size.z = values[8];
      obj[0].coords.setOrigin(orig);
      obj[0].coords.setOrientation(angles[0], angles[1], angles[2]);
      obj[0].object.setSize(size.x, size.y, size.z);
      theScene.objectModified(obj[0].object);
      obj[0].object.sceneChanged(obj[0], theScene);
    }
    else
    {
      dlg = new TransformDialog(this, Translate.text("objectLayoutTitleMultiple"), false, false);
      if (!dlg.clickedOk())
        return;
      values = dlg.getValues();
      for (i = 0; i < sel.length; i++)
      {
        orig = obj[i].coords.getOrigin();
        angles = obj[i].coords.getRotationAngles();
        size = obj[i].object.getBounds().getSize();
        if (!Double.isNaN(values[0]))
          orig.x = values[0];
        if (!Double.isNaN(values[1]))
          orig.y = values[1];
        if (!Double.isNaN(values[2]))
          orig.z = values[2];
        if (!Double.isNaN(values[3]))
          angles[0] = values[3];
        if (!Double.isNaN(values[4]))
          angles[1] = values[4];
        if (!Double.isNaN(values[5]))
          angles[2] = values[5];
        if (!Double.isNaN(values[6]))
          size.x = values[6];
        if (!Double.isNaN(values[7]))
          size.y = values[7];
        if (!Double.isNaN(values[8]))
          size.z = values[8];
        obj[i].coords.setOrigin(orig);
        obj[i].coords.setOrientation(angles[0], angles[1], angles[2]);
        obj[i].object.setSize(size.x, size.y, size.z);
      }
      for (i = 0; i < sel.length; i++)
      {
        theScene.objectModified(obj[i].object);
        obj[i].object.sceneChanged(obj[i], theScene);
      }
    }
    setUndoRecord(undo);
    updateImage();
  }

  public void transformObjectCommand()
  {
    int i, sel[] = theScene.getSelection();
    TransformDialog dlg;
    ObjectInfo info;
    Object3D obj;
    CoordinateSystem coords;
    Vec3 orig, size, center;
    double values[];
    Mat4 m;

    if (sel.length == 0)
      return;
    if (sel.length == 1)
      dlg = new TransformDialog(this, Translate.text("transformObjectTitle", theScene.getObject(sel[0]).name), 
		new double [] {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0}, true, true);
    else
      dlg = new TransformDialog(this, Translate.text("transformObjectTitleMultiple"), 
		new double [] {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0}, true, true);
    if (!dlg.clickedOk())
      return;
    values = dlg.getValues();

    // Find the center of all selected objects.

    BoundingBox bounds = null;
    for (i = 0; i < sel.length; i++)
    {
      info = theScene.getObject(sel[i]);
      if (bounds == null)
        bounds = info.getBounds().transformAndOutset(info.coords.fromLocal());
      else
        bounds = bounds.merge(info.getBounds().transformAndOutset(info.coords.fromLocal()));
    }
    center = bounds.getCenter();
    if (dlg.applyToChildren())
      sel = theScene.getSelectionWithChildren();

    // Determine the rotation matrix.

    m = Mat4.identity();
    if (!Double.isNaN(values[3]))
      m = m.times(Mat4.xrotation(values[3]*Math.PI/180.0));
    if (!Double.isNaN(values[4]))
      m = m.times(Mat4.yrotation(values[4]*Math.PI/180.0));
    if (!Double.isNaN(values[5]))
      m = m.times(Mat4.zrotation(values[5]*Math.PI/180.0));
    UndoRecord undo = new UndoRecord(this, false);
    for (i = 0; i < sel.length; i++)
    {
      info = theScene.getObject(sel[i]);
      obj = info.object;
      coords = info.coords;
      undo.addCommand(UndoRecord.COPY_OBJECT, new Object [] {obj, obj.duplicate()});
      undo.addCommand(UndoRecord.COPY_COORDS, new Object [] {coords, coords.duplicate()});
      orig = coords.getOrigin();
      size = obj.getBounds().getSize();
      if (!Double.isNaN(values[0]))
        orig.x += values[0];
      if (!Double.isNaN(values[1]))
        orig.y += values[1];
      if (!Double.isNaN(values[2]))
        orig.z += values[2];
      if (!Double.isNaN(values[6]))
        size.x *= values[6];
      if (!Double.isNaN(values[7]))
        size.y *= values[7];
      if (!Double.isNaN(values[8]))
        size.z *= values[8];
      if (dlg.useSelectionCenter())
      {
        Vec3 neworig = orig.minus(center);
        if (!Double.isNaN(values[6]))
          neworig.x *= values[6];
        if (!Double.isNaN(values[7]))
          neworig.y *= values[7];
        if (!Double.isNaN(values[8]))
          neworig.z *= values[8];
        coords.setOrigin(neworig);
        coords.transformCoordinates(m);
        coords.setOrigin(coords.getOrigin().plus(center));
      }
      else
      {
        coords.setOrigin(orig);
        coords.transformAxes(m);
      }
      obj.setSize(size.x, size.y, size.z);
    }
    for (i = 0; i < sel.length; i++)
    {
      info = theScene.getObject(sel[i]);
      theScene.objectModified(info.object);
      info.object.sceneChanged(info, theScene);
    }
    setUndoRecord(undo);
    updateImage();
  }

  public void alignObjectsCommand()
  {
    int i, sel[] = theScene.getSelection();
    ComponentsDialog dlg;
    ObjectInfo info;
    Object3D obj;
    CoordinateSystem coords;
    Vec3 alignTo, orig, center;
    double values[];
    BComboBox xchoice, ychoice, zchoice;
    RowContainer px = new RowContainer(), py = new RowContainer(), pz = new RowContainer();
    ValueField vfx, vfy, vfz;
    BoundingBox bounds;

    if (sel.length == 0)
      return;
    px.add(xchoice = new BComboBox(new String [] {
      Translate.text("doNotAlign"),
      Translate.text("Right"),
      Translate.text("Center"),
      Translate.text("Left"),
      Translate.text("Origin")
    }));
    px.add(Translate.label("alignTo"));
    px.add(vfx = new ValueField(Double.NaN, ValueField.NONE, 5));
    py.add(ychoice = new BComboBox(new String [] {
      Translate.text("doNotAlign"),
      Translate.text("Top"),
      Translate.text("Center"),
      Translate.text("Bottom"),
      Translate.text("Origin")
    }));
    py.add(Translate.label("alignTo"));
    py.add(vfy = new ValueField(Double.NaN, ValueField.NONE, 5));
    pz.add(zchoice = new BComboBox(new String [] {
      Translate.text("doNotAlign"),
      Translate.text("Front"),
      Translate.text("Center"),
      Translate.text("Back"),
      Translate.text("Origin")
    }));
    pz.add(Translate.label("alignTo"));
    pz.add(vfz = new ValueField(Double.NaN, ValueField.NONE, 5));
    dlg = new ComponentsDialog(this, Translate.text("alignObjectsTitle"), 
		new Widget [] {px, py, pz}, new String [] {"X", "Y", "Z"});
    if (!dlg.clickedOk())
      return;
    UndoRecord undo = new UndoRecord(this, false);

    // Determine the position to align the objects to.

    alignTo = new Vec3();
    for (i = 0; i < sel.length; i++)
    {
      info = theScene.getObject(sel[i]);
      coords = info.coords;
      bounds = info.getBounds();
      bounds = bounds.transformAndOutset(coords.fromLocal());
      center = bounds.getCenter();
      orig = coords.getOrigin();
      if (!Double.isNaN(vfx.getValue()))
        alignTo.x += vfx.getValue();
      else if (xchoice.getSelectedIndex() == 1)
        alignTo.x += bounds.maxx;
      else if (xchoice.getSelectedIndex() == 2)
        alignTo.x += center.x;
      else if (xchoice.getSelectedIndex() == 3)
        alignTo.x += bounds.minx;
      else if (xchoice.getSelectedIndex() == 4)
        alignTo.x += orig.x;
      if (!Double.isNaN(vfy.getValue()))
        alignTo.y += vfy.getValue();
      else if (ychoice.getSelectedIndex() == 1)
        alignTo.y += bounds.maxy;
      else if (ychoice.getSelectedIndex() == 2)
        alignTo.y += center.y;
      else if (ychoice.getSelectedIndex() == 3)
        alignTo.y += bounds.miny;
      else if (ychoice.getSelectedIndex() == 4)
        alignTo.y += orig.y;
      if (!Double.isNaN(vfz.getValue()))
        alignTo.z += vfz.getValue();
      else if (zchoice.getSelectedIndex() == 1)
        alignTo.z += bounds.maxz;
      else if (zchoice.getSelectedIndex() == 2)
        alignTo.z += center.z;
      else if (zchoice.getSelectedIndex() == 3)
        alignTo.z += bounds.minz;
      else if (zchoice.getSelectedIndex() == 4)
        alignTo.z += orig.z;
    }
    alignTo.scale(1.0/sel.length);

    // Now transform all of the objects.

    for (i = 0; i < sel.length; i++)
    {
      info = theScene.getObject(sel[i]);
      coords = info.coords;
      bounds = info.getBounds();
      bounds = bounds.transformAndOutset(coords.fromLocal());
      center = bounds.getCenter();
      orig = coords.getOrigin();
      undo.addCommand(UndoRecord.COPY_COORDS, new Object [] {coords, coords.duplicate()});
      if (xchoice.getSelectedIndex() == 1)
        orig.x += alignTo.x-bounds.maxx;
      else if (xchoice.getSelectedIndex() == 2)
        orig.x += alignTo.x-center.x;
      else if (xchoice.getSelectedIndex() == 3)
        orig.x += alignTo.x-bounds.minx;
      else if (xchoice.getSelectedIndex() == 4)
        orig.x += alignTo.x-orig.x;
      if (ychoice.getSelectedIndex() == 1)
        orig.y += alignTo.y-bounds.maxy;
      else if (ychoice.getSelectedIndex() == 2)
        orig.y += alignTo.y-center.y;
      else if (ychoice.getSelectedIndex() == 3)
        orig.y += alignTo.y-bounds.miny;
      else if (ychoice.getSelectedIndex() == 4)
        orig.y += alignTo.y-orig.y;
      if (zchoice.getSelectedIndex() == 1)
        orig.z += alignTo.z-bounds.maxz;
      else if (zchoice.getSelectedIndex() == 2)
        orig.z += alignTo.z-center.z;
      else if (zchoice.getSelectedIndex() == 3)
        orig.z += alignTo.z-bounds.minz;
      else if (zchoice.getSelectedIndex() == 4)
        orig.z += alignTo.z-orig.z;
      coords.setOrigin(orig);
    }
    for (i = 0; i < sel.length; i++)
    {
      info = theScene.getObject(sel[i]);
      info.object.sceneChanged(info, theScene);
    }
    setUndoRecord(undo);
    updateImage();
  }

  public void setTextureCommand()
  {
    int sel[] = theScene.getSelection(), i, count = 0;
    ObjectInfo obj[];

    for (i = 0; i < sel.length; i++)
      if (theScene.getObject(sel[i]).object.canSetTexture())
	count++;
    if (count == 0)
      return;
    obj = new ObjectInfo [count];
    for (i = 0; i < sel.length; i++)
      if (theScene.getObject(sel[i]).object.canSetTexture())
	obj[i] = theScene.getObject(sel[i]);
    new ObjectTextureDialog(this, theScene, obj);
    for (i = 0; i < sel.length; i++)
      theScene.objectModified(theScene.getObject(sel[i]).object);
    modified = true;
    updateImage();
  }

  public void setMaterialCommand()
  {
    int sel[] = theScene.getSelection(), i, count = 0;
    ObjectInfo obj[], info;

    for (i = 0; i < sel.length; i++)
      {
	info = theScene.getObject(sel[i]);
	if (info.object.canSetMaterial())
	  count++;
      }
    if (count == 0)
      return;
    obj = new ObjectInfo [count];
    for (i = 0; i < sel.length; i++)
      {
	info = theScene.getObject(sel[i]);
	if (info.object.canSetMaterial())
	  obj[i] = info;
      }
    new ObjectMaterialDialog(this, theScene, obj);
    modified = true;
    updateImage();
  }

  public void renameObjectCommand()
  {
    int sel[] = theScene.getSelection();
    double tol;
    ObjectInfo info;

    if (sel.length != 1)
      return;
    info = theScene.getObject(sel[0]);
    BStandardDialog dlg = new BStandardDialog("", Translate.text("renameObjectTitle"), BStandardDialog.PLAIN);
    String val = dlg.showInputDialog(this, null, info.name);
    if (val == null)
      return;
    setObjectName(sel[0], val);
  }

  public void convertToTriangleCommand()
  {
    int sel[] = theScene.getSelection();
    int i, seli;
    double tol;
    TriangleMesh mesh = null;
    ObjectInfo info = null;
    boolean confirmed = false, poseRemConfirmed = false;
    boolean hasPose = false;

    if (sel.length == 0)
      return;

    int convertmode = Object3D.EXACTLY;

    for(seli = 0; seli < sel.length; ++seli)
    {
      info = theScene.getObject(sel[seli]);
      Object3D obj = info.object;
      if (obj.canConvertToTriangleMesh() == Object3D.APPROXIMATELY)
        convertmode = Object3D.APPROXIMATELY;
      else if (obj.canConvertToTriangleMesh() == Object3D.CANT_CONVERT)
      {
        convertmode = Object3D.CANT_CONVERT;
        break;
      }

      if (sel.length == 1)   // TODO(MB)
      {
        // If the object has a Pose track, all Pose keyframes will need to be deleted.
        for (i = 0; i < info.tracks.length; i++)
          if (info.tracks[i] instanceof PoseTrack)
            {
              if (!poseRemConfirmed && !info.tracks[i].isNullTrack())
              {
                hasPose = true;
                BStandardDialog dlg = new BStandardDialog("", Translate.text("convertLosesPosesWarning", info.name), BStandardDialog.QUESTION);
                String options[] = new String []
                {Translate.text("button.ok"), Translate.text("button.cancel")};
                if (dlg.showOptionDialog(this, options, options[0]) == 1)
                  return;
                confirmed = true;
                poseRemConfirmed = true;
              }
              if (info.tracks[i].getTimecourse() != null)
                info.tracks[i].getTimecourse().removeAllTimepoints();
              info.pose = null;
            }
      }
    }

    if (confirmed)
      theScore.repaintAll();
    
    tol = 0.0;  //(MB) Needed for compiler

    if (convertmode == Object3D.EXACTLY)
    {
      if (!confirmed)
      {
        BStandardDialog dlg = new BStandardDialog("", Translate.text("confirmConvertToTriangle", info.name), BStandardDialog.QUESTION);
        String options[] = new String [] {Translate.text("button.ok"), Translate.text("button.cancel")};
        if (dlg.showOptionDialog(this, options, options[0]) == 1)
          return;
      }

      tol = 0.0;
    }
    else if (convertmode == Object3D.APPROXIMATELY)
    {
      ValueField errorField = new ValueField(0.1, ValueField.POSITIVE);
      ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("selectToleranceForMesh"),
          new Widget [] {errorField}, new String [] {Translate.text("maxError")});
      if (!dlg.clickedOk())
        return;

      tol = errorField.getValue();
//      mesh = obj.convertToTriangleMesh(tol);
    }
    else if (convertmode == Object3D.CANT_CONVERT)
    {
      new BStandardDialog("", Translate.text("cannotTriangulate"), BStandardDialog.ERROR).showMessageDialog(this);
      return;
    }

    UndoRecord undo = new UndoRecord(this, false, UndoRecord.COPY_OBJECT_INFO, new Object [] {info, info.duplicate()});

    if (sel.length == 1)
    {
      info = theScene.getObject(sel[0]);
      Object3D obj = info.object;

      mesh = obj.convertToTriangleMesh(tol);

      if (mesh.getTexture() == null)
      {
        Texture tex = theScene.getDefaultTexture();
        mesh.setTexture(tex, tex.getDefaultMapping());
      }
      if (poseRemConfirmed)
        theScene.replaceObject(obj, mesh.getPosableObject(), undo);
      theScene.replaceObject(obj, mesh, undo);
    }
    else
    {
      info = theScene.getObject(sel[0]);
      Object3D obj = info.object.duplicate();
      Mat4 trans = info.getCoordinates().fromLocal();
      TriangleMesh mesh2;

      mesh = obj.convertToTriangleMesh(tol);
      mesh.transformVertices(trans);

      for(seli = 1; seli < sel.length; ++seli)
      {
        info = theScene.getObject(sel[seli]);
        obj = info.object.duplicate();
        trans = info.getCoordinates().fromLocal();
        mesh2 = obj.convertToTriangleMesh(tol);
        mesh2.transformVertices(trans);

        mesh.addGeometry(mesh2);
      }

      mesh.refreshVertexData();
      // Recenter mesh
      Vec3 center = mesh.getBounds().getCenter();
      trans = Mat4.translation(-center.x, -center.y, -center.z);
      mesh.transformVertices(trans);

      Texture tex = theScene.getDefaultTexture();
      mesh.setTexture(tex, tex.getDefaultMapping());

      info = new ObjectInfo(mesh,
          new CoordinateSystem(center, Vec3.vz(), Vec3.vy()), "Combined Mesh");
      info.addTrack(new PositionTrack(info), 0);
      info.addTrack(new RotationTrack(info), 1);

      addObject(info, undo);
    }

    updateImage();
    updateMenus();
  }

  
  public void convertToActorCommand()
  {
    int sel[] = theScene.getSelection();
    double tol;
    Object3D obj, mesh;
    ObjectInfo info;

    if (sel.length != 1)
      return;
    info = theScene.getObject(sel[0]);
    obj = info.object;
    Object3D posable = obj.getPosableObject();
    if (posable == null)
      return;
    BStandardDialog dlg = new BStandardDialog("", UIUtilities.breakString(Translate.text("confirmConvertToActor", info.name)), BStandardDialog.QUESTION);
    String options[] = new String [] {Translate.text("button.ok"), Translate.text("button.cancel")};
    if (dlg.showOptionDialog(this, options, options[0]) == 1)
      return;
    UndoRecord undo = new UndoRecord(this, false, UndoRecord.COPY_OBJECT_INFO, new Object [] {info, info.duplicate()});
    theScene.replaceObject(obj, posable, undo);
    setUndoRecord(undo);
    updateImage();
    updateMenus();
  }

  void setObjectVisibility(boolean visible, boolean selectionOnly)
  {
    int i, sel[];
    ObjectInfo info;

    UndoRecord undo = new UndoRecord(this, false);
    if (selectionOnly)
    {
      sel = theScene.getSelection();
      for (i = 0; i < sel.length; i++)
      {
        info = theScene.getObject(sel[i]);
        undo.addCommand(UndoRecord.COPY_OBJECT_INFO, new Object [] {info, info.duplicate()});
        info.visible = visible;
      }
    }
    else
      for (i = 0; i < theScene.getNumObjects(); i++)
      {
        info = theScene.getObject(i);
        undo.addCommand(UndoRecord.COPY_OBJECT_INFO, new Object [] {info, info.duplicate()});
        info.visible = visible;
      }
    setUndoRecord(undo);
    updateImage();
    itemTree.repaint();
  }
  
  /** List of strings recognized by {@link #createObjectCommand}. Needed
    by {@link #actionPerformed}. */
  final String[] CREATABLEOBJECTS = new String[]
      {
        "cube", "sphere", "cylinder", "cone", "pointLight", "directionalLight",
        "spotLight", "camera", "null"
      };
      
  void createObjectCommand(String type)
  {
    Object3D obj;
    String name;

    if ("cube".equals(type))
    {
      obj = new Cube(1.0, 1.0, 1.0);
      name = "Cube "+(CreateCubeTool.counter++);
    }
    else if ("sphere".equals(type))
    {
      obj = new Sphere(0.5, 0.5, 0.5);
      name = "Sphere "+(CreateSphereTool.counter++);
    }
    else if ("cylinder".equals(type))
    {
      obj = new Cylinder(1.0, 0.5, 0.5, 1.0);
      name = "Cylinder "+(CreateCylinderTool.counter++);
    }
    else if ("cone".equals(type))
    {
      obj = new Cylinder(1.0, 0.5, 0.5, 0.0);
      name = "Cone "+(CreateCylinderTool.counter++);
    }
    else if ("pointLight".equals(type))
    {
      obj = new PointLight(new RGBColor(1.0f, 1.0f, 1.0f), 1.0f, 0.1);
      name = "Light "+(CreateLightTool.counter++);
    }
    else if ("directionalLight".equals(type))
    {
      obj = new DirectionalLight(new RGBColor(1.0f, 1.0f, 1.0f), 1.0f);
      name = "Light "+(CreateLightTool.counter++);
    }
    else if ("spotLight".equals(type))
    {
      obj = new SpotLight(new RGBColor(1.0f, 1.0f, 1.0f), 1.0f, 20.0, 0.0, 0.1);
      name = "Light "+(CreateLightTool.counter++);
    }
    else if ("camera".equals(type))
    {
      obj = new SceneCamera();
      name = "Camera "+(CreateCameraTool.counter++);
    }
    else
    {
      obj = new NullObject();
      name = "Null";
    }
    CoordinateSystem coords = new CoordinateSystem(new Vec3(), Vec3.vz(), Vec3.vy());
    ObjectInfo info = new ObjectInfo(obj, coords, name);
    if (obj.canSetTexture())
      info.setTexture(theScene.getDefaultTexture(), theScene.getDefaultTexture().getDefaultMapping());
    Vec3 orig = coords.getOrigin();
    double angles[] = coords.getRotationAngles();
    Vec3 size = info.getBounds().getSize();
    TransformDialog dlg = new TransformDialog(this, Translate.text("objectLayoutTitle", name), 
	new double [] {orig.x, orig.y, orig.z, angles[0], angles[1], angles[2],
	size.x, size.y, size.z}, false, false);
    if (!dlg.clickedOk())
      return;
    double values[] = dlg.getValues();
    if (!Double.isNaN(values[0]))
      orig.x = values[0];
    if (!Double.isNaN(values[1]))
      orig.y = values[1];
    if (!Double.isNaN(values[2]))
      orig.z = values[2];
    if (!Double.isNaN(values[3]))
      angles[0] = values[3];
    if (!Double.isNaN(values[4]))
      angles[1] = values[4];
    if (!Double.isNaN(values[5]))
      angles[2] = values[5];
    if (!Double.isNaN(values[6]))
      size.x = values[6];
    if (!Double.isNaN(values[7]))
      size.y = values[7];
    if (!Double.isNaN(values[8]))
      size.z = values[8];
    coords.setOrigin(orig);
    coords.setOrientation(angles[0], angles[1], angles[2]);
    obj.setSize(size.x, size.y, size.z);
    info.addTrack(new PositionTrack(info), 0);
    info.addTrack(new RotationTrack(info), 1);
    UndoRecord undo = new UndoRecord(this, false);
    addObject(info, undo);
    setSelection(theScene.getNumObjects()-1);
    setUndoRecord(undo);
    updateImage();
  }

  public void createScriptObjectCommand()
  {
    // Prompt the user to select a name and, optionally, a predefined script.

    BTextField nameField = new BTextField(Translate.text("Script"));
    BComboBox scriptChoice = new BComboBox();
    scriptChoice.add(Translate.text("newScript"));
    String files[] = new File(ModellingApp.OBJECT_SCRIPT_DIRECTORY).list();
    if (files != null)
      for (int i = 0; i < files.length; i++)
        if (files[i].endsWith(".bsh") && files[i].length() > 4)
          scriptChoice.add(files[i].substring(0, files[i].length()-4));
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("newScriptedObject"),
      new Widget [] {nameField, scriptChoice}, new String [] {Translate.text("Name"), Translate.text("Script")});
    if (!dlg.clickedOk())
      return;

    // If they are using a predefined script, load it.

    String scriptText = "";
    if (scriptChoice.getSelectedIndex() > 0)
    {
      try
      {
        String scriptName = scriptChoice.getSelectedValue()+".bsh";
        File f = new File(ModellingApp.OBJECT_SCRIPT_DIRECTORY, scriptName);
        scriptText = ModellingApp.loadFile(f);
      }
      catch (IOException ex)
      {
        new BStandardDialog("", new String [] {Translate.text("errorReadingScript"), ex.getMessage()}, BStandardDialog.ERROR).showMessageDialog(this);
        return;
      }
    }
    ScriptedObject obj = new ScriptedObject("");
    ObjectInfo info = new ObjectInfo(obj, new CoordinateSystem(), nameField.getText());
    UndoRecord undo = new UndoRecord(this, false);
    addObject(info, undo);
    setSelection(theScene.getNumObjects()-1);
    setUndoRecord(undo);
    updateImage();
    obj.setScript(scriptText);
    obj.edit(this, info, null);
  }

  public void jumpToTimeCommand()
  {
    ValueField timeField = new ValueField(theScene.getTime(), ValueField.NONE);
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("jumpToTimeTitle"), 
      new Widget [] {timeField}, new String [] {Translate.text("Time")});

    if (!dlg.clickedOk())
      return;
    double t = timeField.getValue();
    double fps = theScene.getFramesPerSecond();
    t = Math.round(t*fps)/(double) fps;
    setTime(t);
  }

  public void bindToParentCommand()
  {
    BStandardDialog dlg = new BStandardDialog("", UIUtilities.breakString(Translate.text("confirmBindParent")), BStandardDialog.QUESTION);
    String options[] = new String [] {Translate.text("button.ok"), Translate.text("button.cancel")};
    if (dlg.showOptionDialog(this, options, options[0]) == 1)
      return;
    int sel[] = theScene.getSelection();

    UndoRecord undo = new UndoRecord(this, false);
    for (int i = 0; i < sel.length; i++)
    {
      ObjectInfo info = theScene.getObject(sel[i]);
      if (info.parent == null)
        continue;
      Skeleton s = info.parent.getSkeleton();
      ObjectRef relObj = new ObjectRef(info.parent);
      if (s != null)
      {
        double nearest = Double.MAX_VALUE;
        Joint jt[] = s.getJoints();
        Vec3 pos = info.coords.getOrigin();
        for (int j = 0; j < jt.length; j++)
        {
          ObjectRef r = new ObjectRef(info.parent, jt[j]);
          double dist = r.getCoords().getOrigin().distance2(pos);
          if (dist < nearest)
          {
            relObj = r;
            nearest = dist;
          }
        }
      }
      undo.addCommand(UndoRecord.COPY_OBJECT_INFO, new Object [] {info, info.duplicate()});
      PositionTrack pt = new PositionTrack(info);
      pt.setCoordsObject(relObj);
      info.addTrack(pt, 0);
      pt.setKeyframe(theScene.getTime(), theScene);
      RotationTrack rt = new RotationTrack(info);
      rt.setCoordsObject(relObj);
      info.addTrack(rt, 1);
      rt.setKeyframe(theScene.getTime(), theScene);
    }
    setUndoRecord(undo);
    theScore.rebuildList();
    theScore.repaint();
  }

  public void renderCommand()
  {
    new RenderSetupDialog(this, theScene);
  }

  public void renderImmediatelyCommand()
  {
    RenderSetupDialog.renderImmediately(this, theScene);
  }

  public void toggleViewsCommand()
  {
    if (numViewsShown == 4)
    {
      numViewsShown = 1;
      viewsContainer.setRowCount(1);
      viewsContainer.setColumnCount(1);
      viewsContainer.add(viewPanel[currentView], 0, 0);
    }
    else
    {
      numViewsShown = 4;
      viewsContainer.setRowCount(2);
      viewsContainer.setColumnCount(2);
      viewsContainer.add(viewPanel[0], 0, 0);
      viewsContainer.add(viewPanel[1], 1, 0);
      viewsContainer.add(viewPanel[2], 0, 1);
      viewsContainer.add(viewPanel[3], 1, 1);
    }
    viewsContainer.layoutChildren();
    updateMenus();
    updateImage();
    viewPanel[currentView].requestFocus();
  }

  public void setTemplateCommand()
  {
    BFileChooser fc = new BFileChooser(BFileChooser.OPEN_FILE, Translate.text("selectTemplateImage"));
    if (!fc.showDialog(this))
      return;
    File f = fc.getSelectedFile();
    try
    {
      theView[currentView].setTemplateImage(f);
    }
    catch (InterruptedException ex)
    {
      new BStandardDialog("", UIUtilities.breakString(Translate.text("errorLoadingImage", f.getName())), BStandardDialog.ERROR).showMessageDialog(this);
    }
    theView[currentView].setShowTemplate(true);
    updateMenus();
    updateImage();
  }

  public void setGridCommand()
  {
    ValueField spaceField = new ValueField(theScene.getGridSpacing(), ValueField.POSITIVE);
    ValueField divField = new ValueField(theScene.getGridSubdivisions(), ValueField.POSITIVE+ValueField.INTEGER);
    BCheckBox showBox = new BCheckBox(Translate.text("showGrid"), theScene.getShowGrid());
    BCheckBox snapBox = new BCheckBox(Translate.text("snapToGrid"), theScene.getSnapToGrid());
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("gridTitle"), 
		new Widget [] {spaceField, divField, showBox, snapBox}, 
		new String [] {Translate.text("gridSpacing"), Translate.text("snapToSubdivisions"), null, null});
    if (!dlg.clickedOk())
      return;
    theScene.setGridSpacing(spaceField.getValue());
    theScene.setGridSubdivisions((int) divField.getValue());
    theScene.setShowGrid(showBox.getState());
    theScene.setSnapToGrid(snapBox.getState());
    for (int i = 0; i < theView.length; i++)
      theView[i].setGrid(theScene.getGridSpacing(), theScene.getGridSubdivisions(), theScene.getShowGrid(), theScene.getSnapToGrid());
    updateImage();
  }

  public void frameWithCameraCommand(boolean selectionOnly)
  {
    int sel[] = theScene.getSelectionWithChildren();
    BoundingBox bb = null;

    if (selectionOnly)
      for (int i = 0; i < sel.length; i++)
      {
        ObjectInfo info = theScene.getObject(sel[i]);
        BoundingBox bounds = info.getBounds().transformAndOutset(info.coords.fromLocal());
        if (bb == null)
          bb = bounds;
        else
          bb = bb.merge(bounds);
      }
    else
      for (int i = 0; i < theScene.getNumObjects(); i++)
      {
        ObjectInfo info = theScene.getObject(i);
        BoundingBox bounds = info.getBounds().transformAndOutset(info.coords.fromLocal());
        if (bb == null)
          bb = bounds;
        else
          bb = bb.merge(bounds);
      }
    if (bb == null)
      return;
    if (numViewsShown == 1)
      theView[currentView].frameBox(bb);
    else
      for (int i = 0; i < theView.length; i++)
        theView[i].frameBox(bb);
    updateImage();
  }

  public void texturesCommand()
  {
    theScene.showTexturesDialog(this);
  }

  public void materialsCommand()
  {
    theScene.showMaterialsDialog(this);
  }

  public void environmentCommand()
  {
    changeEnvironmentByDialog(theScene);
  }
  
  public void setLiveRenderEnvironmentCommand()
  {
    changeEnvironmentByDialog(theView[currentView].getLiveRenderSubset());
  }
  
  public void resetLiveRenderEnvironmentCommand()
  {
    SceneSubset subset = theView[currentView].getLiveRenderSubset();
    
    subset.setAmbientColor(null);
    subset.setEnvironmentColor(null);
    subset.setEnvironmentMapping(null);
    subset.setEnvironmentParameterValues(new ParameterValue[0]);
    subset.setEnvironmentMode(SceneSubset.ENVIRON_UNDEFINED);
    subset.setEnvironmentTexture(null);
    subset.setFogColor(null);
  }
  
  /** Modifies sceneenv according to the user setting in the dialog. */
  public void changeEnvironmentByDialog(final SceneEnvironmentData sceneenv)
  {
//    final RGBColor ambColor = sceneenv.getAmbientColor(), envColor = sceneenv.getEnvironmentColor(), fogColor = sceneenv.getFogColor();
//    final RGBColor oldAmbColor = ambColor.duplicate(), oldEnvColor = envColor.duplicate(), oldFogColor = fogColor.duplicate();
    final RGBColor ambColor = sceneenv.getAmbientColor().duplicate(), envColor = sceneenv.getEnvironmentColor().duplicate(), fogColor = sceneenv.getFogColor().duplicate();
    final Widget ambPatch = ambColor.getSample(50, 30), envPatch = envColor.getSample(50, 30), fogPatch = fogColor.getSample(50, 30);
    final BCheckBox fogBox = new BCheckBox("Environment Fog", sceneenv.getFogState());
    final ValueField fogField = new ValueField(sceneenv.getFogDistance(), ValueField.POSITIVE);
    final OverlayContainer envPanel = new OverlayContainer();
    final BComboBox envChoice;
    final BButton envButton = new BButton(Translate.text("Choose")+":");
    final BLabel envLabel = new BLabel();
    final Sphere envSphere = new Sphere(1.0, 1.0, 1.0);
    final ObjectInfo envInfo = new ObjectInfo(envSphere, new CoordinateSystem(), "Environment");
    final ComponentsDialog dlg;

    envChoice = new BComboBox(new String [] {
      Translate.text("solidColor"),
      Translate.text("textureDiffuse"),
      Translate.text("textureEmissive")
    });
    envChoice.setSelectedIndex(sceneenv.getEnvironmentMode());
    RowContainer row = new RowContainer();
    row.add(envButton);
    row.add(envLabel);
    envPanel.add(envPatch, 0);
    envPanel.add(row, 1);
    if (sceneenv.getEnvironmentMode() == Scene.ENVIRON_SOLID)
      envPanel.setVisibleChild(0);
    else
      envPanel.setVisibleChild(1);
    envInfo.setTexture(sceneenv.getEnvironmentTexture(), sceneenv.getEnvironmentMapping());
    envSphere.setParameterValues(sceneenv.getEnvironmentParameterValues());
    envLabel.setText(envSphere.getTexture().getName());
    envChoice.addEventLink(ValueChangedEvent.class, new Object()
    {
      void processEvent()
      {
        if (envChoice.getSelectedIndex() == Scene.ENVIRON_SOLID)
          envPanel.setVisibleChild(0);
        else
          envPanel.setVisibleChild(1);
        envPanel.getParent().layoutChildren();
      }
    });
    final Runnable envTextureCallback = new Runnable() {
      public void run()
      {
        envLabel.setText(envSphere.getTexture().getName());
        envPanel.getParent().layoutChildren();
      }
    };
    envButton.addEventLink(CommandEvent.class, new Object()
    {
      void processEvent()
      {
        envPanel.getParent().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        ObjectTextureDialog otd = new ObjectTextureDialog(LayoutWindow.this, theScene, new ObjectInfo [] {envInfo});
        otd.setCallback(envTextureCallback);
        envPanel.getParent().setCursor(Cursor.getDefaultCursor());
      }
    });
    ambPatch.addEventLink(MouseClickedEvent.class, new Object()
    {
      void processEvent()
      {
        new ColorChooser(LayoutWindow.this, Translate.text("ambientColor"), ambColor);
        ambPatch.setBackground(ambColor.getColor());
        ambPatch.repaint();
      }
    });
    envPatch.addEventLink(MouseClickedEvent.class, new Object()
    {
      void processEvent()
      {
        new ColorChooser(LayoutWindow.this, Translate.text("environmentColor"), envColor);
        envPatch.setBackground(envColor.getColor());
        envPatch.repaint();
      }
    });
    fogPatch.addEventLink(MouseClickedEvent.class, new Object()
    {
      void processEvent()
      {
        new ColorChooser(LayoutWindow.this, Translate.text("fogColor"), fogColor);
        fogPatch.setBackground(fogColor.getColor());
        fogPatch.repaint();
      }
    });
    Runnable okCallback = new Runnable() {
      public void run()
      {
        sceneenv.setFogColor(fogColor);
        sceneenv.setAmbientColor(ambColor);
        sceneenv.setEnvironmentColor(envColor);
        sceneenv.setFog(fogBox.getState(), fogField.getValue());
        sceneenv.setEnvironmentParameterValues(envSphere.getParameterValues());   //(MB) ???
        sceneenv.setEnvironmentMode(envChoice.getSelectedIndex());
        sceneenv.setEnvironmentTexture(envSphere.getTexture());
        sceneenv.setEnvironmentMapping(envSphere.getTextureMapping());
        
        modified = true;
      }
    };
    Runnable cancelCallback = new Runnable() {
      public void run()
      {
//        ambColor.copy(oldAmbColor);
//        envColor.copy(oldEnvColor);
//        fogColor.copy(oldFogColor);
      }
    };
    dlg = new ComponentsDialog(LayoutWindow.this, Translate.text("environmentTitle"), 
	new Widget [] {ambPatch, envChoice, envPanel, fogBox, fogPatch, fogField}, 
	new String [] {Translate.text("ambientColor"), Translate.text("environment"), "", "", Translate.text("fogColor"), Translate.text("fogDistance")},
        okCallback, cancelCallback);
  }
  
  private void executeScriptCommand(CommandEvent ev)
  {
    executeScript(new File(ev.getActionCommand()));
  }

  /** Execute the tool script contained in a file, passing a reference to this window in its "window" variable. */

  public void executeScript(File f)
  {
    // Read the script from the file.

    String scriptText = null;
    try
    {
      scriptText = ModellingApp.loadFile(f);
    }
    catch (IOException ex)
    {
      new BStandardDialog("", new String [] {Translate.text("errorReadingScript"), ex.getMessage()}, BStandardDialog.ERROR).showMessageDialog(this);
      return;
    }
    try
    {
      ToolScript script = ScriptRunner.parseToolScript(scriptText);
      script.execute(this);
    }
    catch (Exception e)
    {
      ScriptRunner.displayError(e, 1);
    }
    updateImage();
  }
}