/* 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.math.*;
import artofillusion.object.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
// import java.awt.*;
import java.awt.Insets;
import java.io.*;
import java.util.*;

/** The MeshEditorWindow class represents the window for editing Mesh objects.  This is an
    abstract class, with subclasses for various types of objects. */

public abstract class MeshEditorWindow extends BFrame implements
    EditingWindow, MenuItemFactory
{
  protected EditingWindow parentWindow;
  protected MeshViewer theView[];
  BorderContainer viewPanel[];
  GridContainer viewsContainer;
  int numViewsShown, currentView;
  MeshSelectionHolder baseSelHolder;
  SkeletonControlHolder baseSkelControlHolder;

  protected BMenu viewMenu;
  protected ToolPalette tools;
  protected BorderContainer toolsPanel;
  protected EditingTool defaultTool, currentTool, metaTool, altTool;

  protected BLabel helpText;
  protected Object3D oldObject;
  protected BMenuBar menubar;
  // CheckboxMenuItem displayItem[], coordsItem[], showItem[];
  protected UndoStack undoStack;
  /** List of all MenuItem and CheckboxMenuItem used for the editor window. */
  protected MenuWidget[] menuitemsCache;
  protected Runnable onClose;
  protected ObjectInfo objInConstructor;  // TODO(MB) Remove this

  /** 'Or' it to bit mask, if topology changing is allowed. Constant for some derived classes. */
  public static final int OPTION_ALLOWTOPOLOGY = 1;

  /** 'Or' it to bit mask, if window should allow to store/discard changes with "Ok" or "Cancel".
    Constant for some derived classes. */
  public static final int OPTION_ALLOWSOKCANCEL = 2;

  protected static int meshTension = 2;
  protected static int tensionDistance = 0;
  protected static final double[] tensionArray = {5.0, 3.0, 2.0, 1.0, 0.5};
  static int lastNumViewsShown = 1;

  public MeshEditorWindow(EditingWindow parent, String title, ObjectInfo info, Runnable onClose)
  {
    super(title);
    objInConstructor = info;
    parentWindow = parent;
//    oldMesh = (Mesh) obj;
    System.out.println("MeshEditorWindow.MeshEditorWindow1 "+info.getObject3D().getClass().getName());
    oldObject = info.getObject3D().duplicate();
    System.out.println("MeshEditorWindow.MeshEditorWindow2 "+oldObject.getClass().getName());
    undoStack = new UndoStack();
    info.getObject3D().prepareForEditing();

//    if (ModellingApp.defaultFont != null)
//      setFont(ModellingApp.defaultFont);
    if (ModellingApp.APP_ICON != null)
      ((java.awt.Frame) getComponent()).setIconImage(ModellingApp.APP_ICON);

    
    addEventLink(WindowClosingEvent.class, new Object() {
      public void processEvent()
      {
        BStandardDialog dlg = new BStandardDialog("", Translate.text("saveWindowChanges"), BStandardDialog.QUESTION);
        String options[] = new String [] {
          Translate.text("saveChanges"),
          Translate.text("discardChanges"),
          Translate.text("button.cancel")
        };
        int choice = dlg.showOptionDialog(MeshEditorWindow.this, options, options[0]);
        if (choice == 0)
          doOk();
        else if (choice == 1)
          doCancel();
      }
    } );


    // Initialize the four possible viewers

    numViewsShown = 4;
    currentView = 0;
    theView = new MeshViewer [4];
    viewPanel = new BorderContainer [4];
    Object listen = new Object() {
      public void processEvent(MouseClickedEvent ev)
      {
	for (int i = 0; i < theView.length; i++)
	  if (currentView != i && ev.getSource() == theView[i])
          {
            theView[currentView].setDrawFocus(false);
            theView[i].setDrawFocus(true);
            currentView = i;
            updateMenus();
            updateImage();
          }
      }
    };

    // Initialize skel. control holder
    RowContainer p;
    baseSkelControlHolder = new SkeletonControlHolder(info);
    for (int i = 0; i < 4; i++)
    {
      viewPanel[i] = new BorderContainer();
      viewPanel[i].add(p = new RowContainer(), BorderContainer.NORTH);
      viewPanel[i].add(theView[i] = createMeshViewer(info, p), BorderContainer.CENTER);
      theView[i].setMetaTool(getMetaTool());
      theView[i].setAltTool(getAltTool());
      theView[i].setSkeletonControlHolder(baseSkelControlHolder);

//	theView[i].setGrid(theScene.gridSpacing, theScene.gridSubdivisions, theScene.showGrid, theScene.snapToGrid);
      theView[i].addEventLink(MouseClickedEvent.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));

    // Lay out the editor window
    this.onClose = onClose;

    FormContainer content = new FormContainer(new double [] {0.0, 1.0}, new double [] {0.0, 1.0, 0.0, 0.0});
    setContent(content);
    content.setDefaultLayout(new LayoutInfo(LayoutInfo.CENTER, LayoutInfo.BOTH, null, null));
    content.add(helpText = new BLabel(), 0, 3, 2, 1);
    FormContainer top = new FormContainer(new double [] {0.0, 1.0, 0.0, 0.0}, new double [] {1.0});
    content.add(top, 1, 0);
    RowContainer viewControls = new RowContainer();
    top.add(viewControls, 0, 0);
    content.add(viewsContainer, 1, 1, 1, 2);
    top.add(Translate.button("ok", this, "doOk"), 2, 0);
    top.add(Translate.button("cancel", this, "doCancel"), 3, 0);
    UIUtilities.applyDefaultFont(content);
    UIUtilities.applyDefaultBackground(content);

    initBaseSelectionHolder(info);
    initWindowMenus(info);
    toolsPanel = createToolbarPanel();
    System.out.println("MeshEditorWindow toolsPanel"+toolsPanel);
    content.add(toolsPanel, 0, 0, 1, 3);

//    helpText.setBackground(ModellingApp.APP_BACKGROUND_COLOR);
//    p2.setBackground(ModellingApp.APP_BACKGROUND_COLOR);
//    p3.setBackground(ModellingApp.APP_BACKGROUND_COLOR);
//    toolsPanel.setBackground(ModellingApp.APP_BACKGROUND_COLOR);

    
    if (lastNumViewsShown == 1)
    {
      toggleViewsCommand();
    }
    else if (lastNumViewsShown == 4)   // TODO(MB) Better solution
    {
      toggleViewsCommand();
      toggleViewsCommand();
    }
    UIUtilities.recursivelyAddKeyPressedListeners(this);
  }

  /** Tests if menu item item should be shown in the menus for
    this object editor. */
  protected boolean isMenuItemAllowed(MenuWidget item)
  {
    return true;
  }

  /** List of names of menu items which should be CheckboxMenuItem.
  Used by {@link #isMenuItemNameCheckbox(String)}.*/

  private static final String[] MENUITEMS_CHECKBOXITEMS=new String[]
    { "wireframeDisplay", "shadedDisplay", "smoothDisplay", "texturedDisplay",
      "transparentDisplay", "coordinateSystem", "localCoords",
      "sceneCoords", "controlMesh", "surface", "skeleton",
      "entireScene", "autosyncDragging", "showCoordinateAxes"
    };


  /** Tests if menu item item should be a checkbox item. */
  protected boolean isMenuItemNameCheckbox(String itemname)
  {
    return (Utilities.findIndexEqual(MENUITEMS_CHECKBOXITEMS, itemname) > -1);
  }

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

    @param menudesc MenuDescription containing label, internal name and shortcut

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

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

    if (isMenuItemNameCheckbox(name))
      result = Translate.checkboxMenuItem(name, this, "itemStateChanged", false);
    else
      result = Translate.menuItem(name, this, "actionPerformed");

    if (!isMenuItemAllowed(result))
      return null;
    
    menudesc.applyTo(result);

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

    return result;
  }

  /** The itemnames array
    is a list of names for the menu items. The special name "separator"
    creates a separating line between groups of menu items.
    <P>The method tests automatically if the item is allowed and if
    it should be a checkbox item. Multiple subsequent separators and
    separators at the end of the menu are automatically removed.
  */

  protected BMenu createMenu(Object[] itemnames)
  {
    return (BMenu) createMenuRecurs(itemnames);
  }
    
  /** Internal recursive method to build the menu. */
  
  protected MenuWidget createMenuRecurs(Object[] itemnames)
  {
    boolean separator = false;
    MenuWidget mi;
    BMenu result = Translate.menu((String)itemnames[0]);

    for(int i = 1; i < itemnames.length; ++i)
    {
      if ("separator".equals(itemnames[i]))
      {
        separator = true;  // Actual creation of separators is delayed
        continue;
      }
      mi = null;
      if (itemnames[i] instanceof String)
      {
        MenuDescription desc = new MenuDescription();
        desc.internalName = (String)itemnames[i];
        mi = createMenuItem(desc);
      }
      else if (itemnames[i] instanceof Object[])
	mi = createMenuRecurs((Object[])itemnames[i]);

      if (mi == null)
	continue;

      if (separator)
      {
        result.addSeparator();
        separator = false;
      }

//      if (mi instanceof BMenu)   //(MB) Little hack because Menu.add will otherwise not work  TODO: Needed with buoy?
//        result.add((BMenu)mi);
//      else
      result.add(mi);
    }

    return result;
  }

  protected void createViewMenu()
  {
    viewMenu = createMenu(new Object[]{"view", // Menu title
      new Object[]{"displayMode",              // Title of submenu
        "wireframeDisplay",
        "shadedDisplay",
        "smoothDisplay",
        "texturedDisplay",
        "transparentDisplay" },
      new Object[]{"show",
        "controlMesh",
        "surface",
        "skeleton",
        "entireScene" },
      new Object[]{"coordinateSystem",
        "localCoords",
        "sceneCoords" },
      "setViewSettingsForAllViews",
      "separator",

      "fourViews",
      "autosyncDragging",
      "separator",

      "grid",
      "createNewViewNewSelection",
      "showTemplate",
      "setTemplate" });
    menubar.add(viewMenu);
  }

  public void toggleViewsCommand()
  {
    viewsContainer.removeAll();

    if (numViewsShown == 4)
    {
      for(int i = 0; i < theView.length; ++i)
      {
        theView[i].setVisible(i == currentView);
      }
      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);
      for(int i = 0; i < theView.length; ++i)
        theView[i].setVisible(true);
    }
    
    viewsContainer.layoutChildren();
    updateMenus();
    updateImage();
    viewPanel[currentView].requestFocus();
    lastNumViewsShown = numViewsShown;
  }


  protected BMenu createShowMenu()  // TODO(MB) Remove method
  {
    BMenu menu = Translate.menu("show");
    menu.add(Translate.checkboxMenuItem("controlMesh", this, "itemStateChanged", theView[0].getMeshVisible()));
    menu.add(Translate.checkboxMenuItem("surface", this, "itemStateChanged", theView[0].getSurfaceVisible()));
    menu.add(Translate.checkboxMenuItem("skeleton", this, "itemStateChanged", theView[0].getSkeletonVisible()));
    menu.add(Translate.checkboxMenuItem("entireScene", this, "itemStateChanged", theView[0].getSceneVisible()));
    return menu;
  }

  /** Updates a single menu item
    (setting enabled/disabled, (un)checked ...). */

  protected void updateMenuItem(MenuWidget item)
  {
    String itemname = ((Widget)item).getName();
    MeshViewer currView = getCurrentView();
    BCheckBoxMenuItem cbitem = null;
    BMenuItem mnitem = null;
    if (item instanceof BCheckBoxMenuItem)
      cbitem = (BCheckBoxMenuItem)item;
    else if (item instanceof BMenuItem)
      mnitem = (BMenuItem)item;
    else
      return;

    if ("undo".equals(itemname))
      mnitem.setEnabled(undoStack.canUndo());
    else if ("redo".equals(itemname))
      mnitem.setEnabled(undoStack.canRedo());
    else if ("localCoords".equals(itemname))
      cbitem.setState(!currView.getUseWorldCoords());
    else if ("sceneCoords".equals(itemname))
      cbitem.setState(currView.getUseWorldCoords());
    else if ("wireframeDisplay".equals(itemname))
      cbitem.setState(currView.getRenderMode() == ViewerCanvas.RENDER_WIREFRAME);
    else if ("shadedDisplay".equals(itemname))
      cbitem.setState(currView.getRenderMode() == ViewerCanvas.RENDER_FLAT);
    else if ("smoothDisplay".equals(itemname))
      cbitem.setState(currView.getRenderMode() == ViewerCanvas.RENDER_SMOOTH);
    else if ("texturedDisplay".equals(itemname))
      cbitem.setState(currView.getRenderMode() == ViewerCanvas.RENDER_TEXTURED);
    else if ("transparentDisplay".equals(itemname))
      cbitem.setState(currView.getRenderMode() == ViewerCanvas.RENDER_TRANSPARENT);
    else if ("controlMesh".equals(itemname))
      cbitem.setState(currView.getMeshVisible());
    else if ("surface".equals(itemname))
      cbitem.setState(currView.getSurfaceVisible());
    else if ("skeleton".equals(itemname))
      cbitem.setState(currView.getSkeletonVisible());
    else if ("entireScene".equals(itemname))
      cbitem.setState(currView.getSceneVisible());
    else if ("autosyncDragging".equals(itemname))
      cbitem.setState(currView.getDraggingAutosync());
    else if ("showTemplate".equals(itemname))
    {
      if (currView.getTemplateImage() == null)
        mnitem.setEnabled(false);
      else
      {
        mnitem.setEnabled(true);
        boolean wasShown = currView.getTemplateShown();
        mnitem.setText(Translate.text(wasShown ? "menu.showTemplate" :
          "menu.hideTemplate"));
      }
    }
    else if ("showCoordinateAxes".equals(itemname))
      cbitem.setState(currView.getShowAxes());
  }

  public void updateMenus()
  {
    if (menuitemsCache == null)
      menuitemsCache = UIUtilities.getMenuItemList(getMenuBar());
    for(int i = 0; i < menuitemsCache.length; ++i)
      updateMenuItem(menuitemsCache[i]);
  }

  public MeshViewer getCurrentView()
  {
    return theView[currentView];
  }

  /** Create a MeshViewer which is appropriate for a particular subclass.
    E. g. a TubeViewer for a TubeEditorWindow. */

  public abstract MeshViewer createMeshViewer(ObjectInfo info, RowContainer p);

  public abstract void initBaseSelectionHolder(ObjectInfo info);

  public void initWindowMenus(ObjectInfo obj)
  {} // Most windows do not need it anymore

  public EditingTool getMetaTool()
  {
    if (metaTool == null)
      metaTool = new MoveViewTool(this);

    return metaTool;
  }

  public EditingTool getAltTool()
  {
    if (altTool == null)
      altTool = new RotateViewTool(this);

    return altTool;
  }

  /** Create the toolbar panel on the left of an object editor. */

  public abstract BorderContainer createToolbarPanel();  // TODO(MB) Why not return Widget?

  /* EditingWindow methods. */

  public boolean confirmClose()
  {
    return true;
  }

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

  /** Sets the scene for all views. */
  public void setSceneForViews(Scene sc, ObjectInfo thisObject)
  {
    for(int i=0; i < theView.length; ++i)
      theView[i].setScene(sc, thisObject);
  }

  /** Sets the autosync dragging mode for all views. */
  public void setDraggingAutosyncForViews(boolean autosyncDragging)
  {
    for(int i=0; i < theView.length; ++i)
      theView[i].setDraggingAutosync(autosyncDragging);
  }

  public BFrame getFrame()
  {
    return this;
  }

  public void updateImage()
  {

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

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

  /** Returns true iff it "knows" a menu command and calls it.
   If the method is overridden by subclasses of MeshEditorWindow
   the super method should be called. */   // TODO(MB) Something is bad here
  protected boolean handleMenuAction(String command)
  {
    if (callCommandMethod(command+"Command"))
      return true;
    else if (command.equals("ok"))
      doOk();
    else if (command.equals("cancel"))
      doCancel();

    return false;
  }

  /** Tries to call a method with no parameters and void return value.
    @param methodname name of the method to call
    @return true iff the method was found and called
  */
  protected boolean callCommandMethod(String methodname)
  {
    try
    {
      //System.out.println("MeshEditorWindow.callCommandMethod "+methodname);
      getClass().getMethod(methodname, null).invoke(this, new Object[0]);
      return true;
    }
    catch(NoSuchMethodException nme)
    {
      return false;
    }
    catch(java.lang.reflect.InvocationTargetException ite)
    {
      ite.getTargetException().printStackTrace();
      return false;
    }
    catch(Exception e)  // TODO(MB) Refine exception handling
    {
      e.printStackTrace();
      return false;
    }
  }
  /** Creates a new view, containing the same Object3D with a
   new VertexSelection. */
  protected abstract MeshEditorWindow createNewViewNewSelection();

  /** Creates a new view, containing the same Object3D and the
   same selection */
  protected MeshEditorWindow createNewViewSameSelection()
  {
    return null;
  } // TODO(MB) Implement

  public Scene getScene()
  {
    return getCurrentView().getScene();
  }

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

    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, getCurrentView());
  }

  /** Convenience method. */
  protected void object3DChangedDuringEditor()
  {
    ((MeshViewer) getCurrentView()).getObject().object.informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITOR);
  }
  public void undoCommand()
  {
    undoStack.executeUndo();
    ((MeshViewer) getCurrentView()).setMesh((Mesh)getCurrentView().getObject().object);
    updateImage();
    updateMenus();
  }
  
  void redoCommand()
  {
    undoStack.executeRedo();
    ((MeshViewer) getCurrentView()).setMesh((Mesh)getCurrentView().getObject().object);
    updateImage();
    updateMenus();
    object3DChangedDuringEditor();
  }

  /** Called if "Ok" was pressed. */
  public void doOk()
  {
    Object3D theMesh = getCurrentView().getObject().getObject3D();
    if (oldObject.getMaterial() != null)
    {
      if (!theMesh.isClosed())
      {
        // setCursor(java.awt.Cursor.getDefaultCursor());
        String options[] = new String [] {Translate.text("button.ok"), Translate.text("button.cancel")};
        BStandardDialog dlg = new BStandardDialog("", UIUtilities.breakString(Translate.text("surfaceNoLongerClosed")), BStandardDialog.WARNING);
        int choice = dlg.showOptionDialog(this, options, options[0]);
        if (choice == 1)
          return;
        theMesh.setMaterial(null, null);
      }
      else
      {
        // TODO(MB) Check if these lines can be removed
        // theMesh.setMaterial(oldObjectInfo.getObject3D().getMaterial());
        // theMesh.setMaterialMapping(oldObjectInfo.getObject3D().getMaterialMapping());
        // respectively this:
        // theMesh.setMaterial(((SplineMesh) oldMesh).getMaterial(), ((SplineMesh) oldMesh).getMaterialMapping());
      }
    }
    theMesh.unprepareAfterEditing();
    dispose();
    onClose.run();
    theMesh.informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITORCOMMIT);
    parentWindow.updateImage();
    parentWindow.updateMenus();
    for(int i = 0; i < theView.length; ++i)
      theView[i].dispose();
  }

  /** Call if "Cancel" was pressed. */
  public void doCancel()
  {
    Object3D theMesh = getCurrentView().getObject().getObject3D();
    theMesh.copyObject(oldObject);
    theMesh.informChanged(this, ModelEvent.CHANGEDUR_OBJECTEDITORROLLBACK);
    theMesh.unprepareAfterEditing();
    dispose();
    for(int i = 0; i < theView.length; ++i)
      theView[i].dispose();
  }


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

  public ViewerCanvas getView()
  {
    return getCurrentView();   // TODO(MB)  Right?
  }

  public void itemStateChanged(CommandEvent e)
  {
    Object source = e.getSource();
    BCheckBoxMenuItem sourceitem = null;
    MeshViewer currView = getCurrentView();
    String itemname = "";

    if (source instanceof BCheckBoxMenuItem)
    {
      sourceitem = (BCheckBoxMenuItem)source;
      itemname = sourceitem.getName();
    }

    if ("localCoords".equals(itemname) || "sceneCoords".equals(itemname))
      currView.setUseWorldCoords("sceneCoords".equals(itemname));
    else if ("controlMesh".equals(itemname))
      currView.setMeshVisible(sourceitem.getState());
    else if ("surface".equals(itemname))
      currView.setSurfaceVisible(sourceitem.getState());
    else if ("skeleton".equals(itemname))
      currView.setSkeletonVisible(sourceitem.getState());
    else if ("entireScene".equals(itemname))
      currView.setSceneVisible(sourceitem.getState());
    else if ("autosyncDragging".equals(itemname))
      setDraggingAutosyncForViews(sourceitem.getState());
    else if ("wireframeDisplay".equals(itemname))
      currView.setRenderMode(ViewerCanvas.RENDER_WIREFRAME);
    else if ("shadedDisplay".equals(itemname))
      currView.setRenderMode(ViewerCanvas.RENDER_FLAT);
    else if ("smoothDisplay".equals(itemname))
      currView.setRenderMode(ViewerCanvas.RENDER_SMOOTH);
    else if ("texturedDisplay".equals(itemname))
      currView.setRenderMode(ViewerCanvas.RENDER_TEXTURED);
    else if ("transparentDisplay".equals(itemname))
      currView.setRenderMode(ViewerCanvas.RENDER_TRANSPARENT);
    else if ("showCoordinateAxes".equals(itemname))
    {
      for(int i = 0; i < theView.length; ++i)
        theView[i].setShowAxes(sourceitem.getState());
    }

    updateMenus();
    updateImage();
  }

  /** Allow the user to enter new coordinates for one or more vertices. */

  public void setPointsCommand()
  {
    int i, j = 0, num = 0;
    final Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    Mesh oldMesh = (Mesh) theMesh.duplicate();
    Skeleton s = theMesh.getSkeleton();
    Joint jt[] = null;
    final int selected[] = getCurrentView().getSelectionDistance();
    final CoordinateSystem coords = getCurrentView().thisObjectInScene.coords;
    final MeshVertex vert[] = theMesh.getVertices();
    final Vec3 points[] = new Vec3 [vert.length];
    final ValueField xField, yField, zField;
    ValueSlider weightSlider = null;
    BComboBox jointChoice = null;
    double weight = -1.0;
    int joint = -2;
    String title;
    ComponentsDialog dlg;

    for (i = 0; i < selected.length; i++)
      if (selected[i] == 0)
      {
        num++;
        j = i;
        if (weight == -1.0)
          weight = vert[i].ikWeight;
        else if (vert[i].ikWeight != weight)
          weight = Double.NaN;
        if (joint == -2)
          joint = vert[i].ikJoint;
        else if (vert[i].ikJoint != joint)
          joint = -3;
      }
    if (num == 0)
      return;
    if (num == 1)
    {
      Vec3 pos = vert[j].r;
      if (getCurrentView().getUseWorldCoords() && coords != null)
        pos = coords.fromLocal().times(pos);
      xField = new ValueField(pos.x, ValueField.NONE, 5);
      yField = new ValueField(pos.y, ValueField.NONE, 5);
      zField = new ValueField(pos.z, ValueField.NONE, 5);
      title = Translate.text("editVertSingle");
    }
    else
    {
      xField = new ValueField(Double.NaN, ValueField.NONE, 5);
      yField = new ValueField(Double.NaN, ValueField.NONE, 5);
      zField = new ValueField(Double.NaN, ValueField.NONE, 5);
      title = Translate.text("editVertMultiple");
    }
    Object listener = new Object() {
      void processEvent()
      {
        for (int i = 0; i < selected.length; i++)
        {
          points[i] = vert[i].r;
          if (selected[i] == 0)
          {
            if (getCurrentView().getUseWorldCoords() && coords != null)
              coords.fromLocal().transform(points[i]);
            if (!Double.isNaN(xField.getValue()))
              points[i].x = xField.getValue();
            if (!Double.isNaN(yField.getValue()))
              points[i].y = yField.getValue();
            if (!Double.isNaN(zField.getValue()))
              points[i].z = zField.getValue();
            if (getCurrentView().getUseWorldCoords() && coords != null)
              coords.toLocal().transform(points[i]);
          }
        }
        theMesh.setVertexPositions(points);
        for(int i = 0; i < theView.length; ++i)
          theView[i].setMesh(theMesh);
        updateImage();
        // ??? theView.repaint();
      }
    };
    xField.addEventLink(ValueChangedEvent.class, listener);
    yField.addEventLink(ValueChangedEvent.class, listener);
    zField.addEventLink(ValueChangedEvent.class, listener);
    if (s == null)
      dlg = new ComponentsDialog(this, title, new Widget [] {xField, yField, zField},
        new String [] {"X", "Y", "Z"});
    else
    {
      weightSlider = new ValueSlider(0.0, 1.0, 100, weight);
      jointChoice = new BComboBox();
      jointChoice.add(Translate.text("none"));
      jt = s.getJoints();
      for (i = 0; i < jt.length; i++)
        jointChoice.add(jt[i].name);
      if (joint == -3)
        jointChoice.add("");
      jointChoice.setSelectedIndex(0);
      for (i = 0; i < jt.length; i++)
        if (jt[i].id == joint)
          jointChoice.setSelectedIndex(i+1);
      if (joint == -3)
        jointChoice.setSelectedIndex(jt.length+1);
      dlg = new ComponentsDialog(this, title, new Widget [] {xField, yField, zField, jointChoice, weightSlider},
        new String [] {"X", "Y", "Z", Translate.text("ikBone"), Translate.text("ikWeight")});
    }
    if (dlg.clickedOk())
    {
      for (i = 0; i < selected.length; i++)
        if (selected[i] == 0)
        {
          if (weightSlider != null && !Double.isNaN(weightSlider.getValue()))
            vert[i].ikWeight = weightSlider.getValue();
          if (jointChoice != null)
          {
            if (jointChoice.getSelectedIndex() == 0)
              vert[i].ikJoint = -1;
            else if (jointChoice.getSelectedIndex() <= jt.length)
              vert[i].ikJoint = jt[jointChoice.getSelectedIndex()-1].id;
          }
        }
      setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, oldMesh}));
        object3DChangedDuringEditor();
    }
    else
    {
      theMesh.copyObject((Object3D) oldMesh);
      for(i = 0; i < theView.length; ++i)
        theView[i].setMesh(theMesh);
      updateImage();
        // ??? theView.repaint();
    }
  }

  /* Allow the user to transform one or more vertices. */

  public void transformPointsCommand()
  {
    int i, j;
    Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    int selected[] = getCurrentView().getSelectionDistance();
    MeshVertex vert[] = theMesh.getVertices();
    Vec3 points[] = new Vec3 [vert.length], center;
    CoordinateSystem coords = getCurrentView().thisObjectInScene.coords;

    for (i = 0; i < selected.length && selected[i] == -1; i++);
    if (i == selected.length)
      return;
    
    // Create the dialog.
    
    FormContainer content = new FormContainer(4, 5);
    LayoutInfo eastLayout = new LayoutInfo(LayoutInfo.EAST, LayoutInfo.NONE, new Insets(0, 0, 0, 5), null);
    content.add(Translate.label("Move"), 0, 1, eastLayout);
    content.add(Translate.label("Rotate"), 0, 2, eastLayout);
    content.add(Translate.label("Scale"), 0, 3, eastLayout);
    content.add(new BLabel("X"), 1, 0);
    content.add(new BLabel("Y"), 2, 0);
    content.add(new BLabel("Z"), 3, 0);
    ValueField fields[] = new ValueField[9];
    for (i = 0; i < 9; i++)
      content.add(fields[i] = new ValueField(Double.NaN, ValueField.NONE), (i%3)+1, (i/3)+1);
    RowContainer row = new RowContainer();
    content.add(row, 0, 4, 4, 1);
    row.add(Translate.label("transformAround"));
    BComboBox centerChoice = new BComboBox(new String [] {
      Translate.text("centerOfSelection"),
      Translate.text("objectOrigin")
    });
    row.add(centerChoice);
    PanelDialog dlg = new PanelDialog(this, Translate.text("transformPoints"), content);
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_VERTEX_POSITIONS, new Object [] {theMesh, theMesh.getVertexPositions()}));
    double val[] = new double [9];
    for (i = 0; i < val.length; i++)
    {
      val[i] = fields[i].getValue();
      if (Double.isNaN(val[i]))
        val[i] = (i < 6 ? 0.0 : 1.0);
    }
    Mat4 m = Mat4.translation(val[0], val[1], val[2]);
    m = m.times(Mat4.xrotation(val[3]*Math.PI/180.0));
    m = m.times(Mat4.yrotation(val[4]*Math.PI/180.0));
    m = m.times(Mat4.zrotation(val[5]*Math.PI/180.0));
    m = m.times(Mat4.scale(val[6], val[7], val[8]));
    if (getCurrentView().getUseWorldCoords() && coords != null)
      m = coords.toLocal().times(m).times(coords.fromLocal());
    if (centerChoice.getSelectedIndex() == 0)
    {
      center = new Vec3();
      for (i = j = 0; i < selected.length; i++)
        if (selected[i] == 0)
        {
          center.add(vert[i].r);
          j++;
        }
      center.scale(1.0/j);
      m = Mat4.translation(center.x, center.y, center.z).times(m).times(Mat4.translation(-center.x, -center.y, -center.z));
    }
    for (i = 0; i < selected.length; i++)
    {
      points[i] = vert[i].r;
      if (selected[i] == 0)
        points[i] = m.times(points[i]);
    }
    theMesh.setVertexPositions(points);
    object3DChangedDuringEditor();
  }

  /* Displace selected vertices by a random amount. */

  public void randomizeCommand()
  {
    int i;
    Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    int selected[] = getCurrentView().getSelectionDistance();
    CoordinateSystem coords = getCurrentView().thisObjectInScene.coords;
    MeshVertex vert[] = theMesh.getVertices();
    Vec3 points[] = new Vec3 [vert.length];
    ValueField xfield, yfield, zfield;

    for (i = 0; i < selected.length && selected[i] == -1; i++);
    if (i == selected.length)
      return;
    xfield = new ValueField(0.0, ValueField.NONE);
    yfield = new ValueField(0.0, ValueField.NONE);
    zfield = new ValueField(0.0, ValueField.NONE);
    ComponentsDialog dlg = new ComponentsDialog(this, "Maximum random displacement:", new Widget []
    		{xfield, yfield, zfield}, new String[] {"X", "Y", "Z"});
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_VERTEX_POSITIONS, new Object [] {theMesh, theMesh.getVertexPositions()}));
    for (i = 0; i < selected.length; i++)
    {
      points[i] = vert[i].r;
      if (selected[i] == 0)
      {
        if (getCurrentView().getUseWorldCoords() && coords != null)
          coords.fromLocal().transform(points[i]);
        points[i].x += (1.0-2.0*Math.random())*xfield.getValue();
        points[i].y += (1.0-2.0*Math.random())*yfield.getValue();
        points[i].z += (1.0-2.0*Math.random())*zfield.getValue();
        if (getCurrentView().getUseWorldCoords() && coords != null)
          coords.toLocal().transform(points[i]);
      }
    }
    theMesh.setVertexPositions(points);
    object3DChangedDuringEditor();
  }

  /** Center the mesh. */

  public void centerCommand()
  {
    Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    MeshVertex vert[] = theMesh.getVertices();
    CoordinateSystem coords = getCurrentView().thisObjectInScene.coords;
    Vec3 center = theMesh.getBounds().getCenter(), points[] = new Vec3 [vert.length];

    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_VERTEX_POSITIONS, new Object [] {theMesh, theMesh.getVertexPositions()}));
    if (getCurrentView().getUseWorldCoords() && coords != null)
    {
      coords.fromLocal().transform(center);
      coords.toLocal().transformDirection(center);
    }
    for (int i = 0; i < vert.length; i++)
      points[i] = vert[i].r.minus(center);
    theMesh.setVertexPositions(points);
    Skeleton skeleton = theMesh.getSkeleton();
    if (skeleton != null)
    {
      Joint joint[] = skeleton.getJoints();
      for (int i = 0; i < joint.length; i++)
        joint[i].coords.setOrigin(joint[i].coords.getOrigin().minus(center));
      object3DChangedDuringEditor();
    }
  }

  /* Allow the user to set the mesh tension. */

  public void setTensionCommand()
  {
    ValueField distanceField = new ValueField((double) tensionDistance, ValueField.NONNEGATIVE+ValueField.INTEGER);
    BComboBox tensionChoice = new BComboBox(new String [] {
      Translate.text("VeryLow"),
      Translate.text("Low"),
      Translate.text("Medium"),
      Translate.text("High"),
      Translate.text("VeryHigh")
    });
    tensionChoice.setSelectedIndex(meshTension);
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("setTensionTitle"), 
		new Widget [] {distanceField, tensionChoice}, 
		new String [] {Translate.text("maxDistance"), Translate.text("Tension")});
    if (!dlg.clickedOk())
      return;
    tensionDistance = (int) distanceField.getValue();
    meshTension = tensionChoice.getSelectedIndex();
  }

  public void createNewViewNewSelectionCommand()
  {
    createNewViewNewSelection().setVisible(true);
  }

  public void createNewViewSameSelectionCommand()
  {
    createNewViewSameSelection().setVisible(true);
  }

  public static double getMeshTension()
  {
    return tensionArray[meshTension];
  }

  public static int getTensionDistance()
  {
    return tensionDistance;
  }

  /** Allow the user to set the texture parameters for selected vertices. */

  public void setParametersCommand()
  {
    Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    final MeshVertex vert[] = theMesh.getVertices();
    ObjectInfo info = getCurrentView().getObject();   //(MB) ObjectInfo needed here
    TextureParameter param[] = info.object.getParameters();
    final ParameterValue paramValue[] = info.object.getParameterValues();
    int i, j, k, paramIndex[] = null;
    final int selected[] = getCurrentView().getSelectionDistance();
    double value[];
    String label[];

    for (j = 0; j < selected.length && selected[j] != 0; j++);
    if (j == selected.length)
      return;
    if (param != null)
    {
      // Find the list of per-vertex parameters.
      
      int num = 0;
      for (i = 0; i < param.length; i++)
        if (paramValue[i] instanceof VertexParameterValue)
          num++;
      paramIndex = new int [num];
      for (i = 0, k = 0; k < param.length; k++)
        if (paramValue[k] instanceof VertexParameterValue)
          paramIndex[i++] = k;
    }
    if (paramIndex == null || paramIndex.length == 0)
    {
      new BStandardDialog("", Translate.text("noPerVertexParams"), BStandardDialog.INFORMATION).showMessageDialog(this);
      return;
    }
    value = new double [paramIndex.length];
    for (i = 0; i < paramIndex.length; i++)
    {
      double currentVal[] = ((VertexParameterValue) paramValue[paramIndex[i]]).getValue();
      value[i] = currentVal[j];
      for (k = j; k < selected.length; k++)
        if (selected[k] == 0 && currentVal[k] != value[i])
          value[i] = Double.NaN;
    }

    // Define an inner class used for resetting parameters that represent texture coordinates.

    class ResetButton extends BButton
    {
      VertexParameterValue xparamVal, yparamVal, zparamVal;
      double xvalList[], yvalList[], zvalList[];
      ValueField xfield, yfield, zfield;

      public ResetButton()
      {
	super(Translate.text("Reset"));
	addEventLink(CommandEvent.class, this);
      }

      public void addParam(int index, int type, ValueField field)
      {
	if (type == TextureParameter.X_COORDINATE)
        {
          xparamVal = (VertexParameterValue) paramValue[index];
          xvalList = xparamVal.getValue();
          xfield = field;
        }
	else if (type == TextureParameter.Y_COORDINATE)
        {
          yparamVal = (VertexParameterValue) paramValue[index];
          yvalList = yparamVal.getValue();
          yfield = field;
        }
	else if (type == TextureParameter.Z_COORDINATE)
        {
          zparamVal = (VertexParameterValue) paramValue[index];
          zvalList = zparamVal.getValue();
          zfield = field;
        }
      }

      private void processEvent()
      {
        BStandardDialog dlg = new BStandardDialog("", Translate.text("resetCoordsToPos"), BStandardDialog.QUESTION);
        String options[] = new String [] {Translate.text("button.ok"), Translate.text("button.cancel")};
        int choice = dlg.showOptionDialog(this, options, options[0]);
	if (choice == 1)
	  return;
	double xval = Double.NaN, yval = Double.NaN, zval = Double.NaN;
	for (int ind = 0; ind < selected.length; ind++)
	  if (selected[ind] == 0)
          {
            // Reset the texture coordinates for this vertex.
            
            if (xparamVal != null)
            {
              xvalList[ind] = vert[ind].r.x;
              if (Double.isNaN(xval))
              {
                xval = vert[ind].r.x;
                xfield.setValue(xval);
              }
              else if (xval != vert[ind].r.x)
                xfield.setValue(Double.NaN);
            }
            if (yparamVal != null)
            {
              yvalList[ind] = vert[ind].r.y;
              if (Double.isNaN(yval))
              {
                yval = vert[ind].r.y;
                yfield.setValue(yval);
              }
              else if (yval != vert[ind].r.y)
                yfield.setValue(Double.NaN);
            }
            if (zparamVal != null)
            {
              zvalList[ind] = vert[ind].r.z;
              if (Double.isNaN(zval))
              {
                zval = vert[ind].r.z;
                zfield.setValue(zval);
              }
              else if (zval != vert[ind].r.z)
                zfield.setValue(Double.NaN);
            }
          }
        if (xparamVal != null)
          xparamVal.setValue(xvalList);
        if (yparamVal != null)
          yparamVal.setValue(yvalList);
        if (zparamVal != null)
          zparamVal.setValue(zvalList);
      }
    }

    // Build the panel for editing the values.

    Widget editWidget[] = new Widget [paramIndex.length];
    ColumnContainer content = new ColumnContainer();
    content.setDefaultLayout(new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, null, null));
    LayoutInfo indent1 = new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, new Insets(0, 10, 0, 0), null);
    LayoutInfo indent2 = new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, new Insets(0, 20, 0, 0), null);
    RowContainer coordsPanel = null;
    ResetButton reset = null;
    Object lastOwner = null;
    if (info.object.getTexture() instanceof LayeredTexture)
    {
      // This is a layered texture, so we want to group the parameters by layer.
      
      LayeredMapping map = (LayeredMapping) info.object.getTextureMapping();
      Texture layer[] = map.getLayers();
      for (k = 0; k < layer.length; k++)
      {
        coordsPanel = null;
        content.add(new BLabel(Translate.text("layerLabel", Integer.toString(k+1), layer[k].getName())));
        TextureParameter layerParam[] = map.getLayerParameters(k);
        boolean any = false;
        for (i = 0; i < paramIndex.length; i++)
        {
          // Determine whether this parameter is actually part of this layer.
          
          int m;
          TextureParameter pm = param[paramIndex[i]];
          for (m = 0; m < layerParam.length; m++)
            if (layerParam[m].equals(pm))
              break;
          if (m == layerParam.length)
            continue;
          any = true;
          
          // It is, so add it.

          editWidget[i] = pm.getEditingWidget(value[i]);
          if (pm.type == TextureParameter.NORMAL_PARAMETER)
          {
            RowContainer row = new RowContainer();
            row.add(new BLabel(pm.name));
            row.add(editWidget[i]);
            content.add(row, indent1);
            if (coordsPanel != null)
              coordsPanel.add(reset);
            coordsPanel = null;
            reset = null;
          }
          else if (coordsPanel == null)
          {
            coordsPanel = new RowContainer();
            content.add(Translate.label("texMappingCoords"), indent1);
            content.add(coordsPanel, indent2);
            coordsPanel.add(new BLabel(pm.name));
            coordsPanel.add(editWidget[i]);
            reset = new ResetButton();
            reset.addParam(paramIndex[i], pm.type, (ValueField) editWidget[i]);
          }
          else
          {
            coordsPanel.add(new BLabel(pm.name));
            coordsPanel.add(editWidget[i]);
            reset.addParam(paramIndex[i], pm.type, (ValueField) editWidget[i]);
          }
        }
        if (coordsPanel != null)
          coordsPanel.add(reset);
        if (!any)
          content.add(Translate.label("noLayerPerVertexParams"), indent1);
      }
    }
    else
    {
      // This is a simple texture, so just list off all the parameters.
      
      content.add(new BLabel(Translate.text("Texture")+": "+info.object.getTexture().getName()));
      for (i = 0; i < paramIndex.length; i++)
      {
        TextureParameter pm = param[paramIndex[i]];
        editWidget[i] = pm.getEditingWidget(value[i]);
        if (pm.type == TextureParameter.NORMAL_PARAMETER)
        {
          RowContainer row = new RowContainer();
          row.add(new BLabel(pm.name));
          row.add(editWidget[i]);
          content.add(row, indent1);
          if (coordsPanel != null)
            coordsPanel.add(reset);
          coordsPanel = null;
          coordsPanel = null;
        }
        else if (coordsPanel == null)
        {
          coordsPanel = new RowContainer();
          content.add(Translate.label("texMappingCoords"), indent1);
          content.add(coordsPanel, indent2);
          coordsPanel.add(new BLabel(pm.name));
          coordsPanel.add(editWidget[i]);
          reset = new ResetButton();
          reset.addParam(paramIndex[i], pm.type, (ValueField) editWidget[i]);
        }
        else
        {
          coordsPanel.add(new BLabel(pm.name));
          coordsPanel.add(editWidget[i]);
          reset.addParam(paramIndex[i], pm.type, (ValueField) editWidget[i]);
        }
      }
      if (coordsPanel != null)
        coordsPanel.add(reset);
    }
    PanelDialog dlg = new PanelDialog(this, Translate.text("texParamsForSelectedPoints"), content);
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    for (j = 0; j < editWidget.length; j++)
    {
      double d;
      if (editWidget[j] instanceof ValueField)
        d = ((ValueField) editWidget[j]).getValue();
      else
        d = ((ValueSlider) editWidget[j]).getValue();
      if (!Double.isNaN(d))
      {
        double val[] = ((VertexParameterValue) paramValue[paramIndex[j]]).getValue();
        for (i = 0; i < selected.length; i++)
          if (selected[i] == 0)
            val[i] = d;
        ((VertexParameterValue) paramValue[paramIndex[j]]).setValue(val);
      }
    }
  }
  
  /** Toggle whether the coordinate axes are shown. */
  
  void showAxesCommand()
  {
    boolean wasShown = getCurrentView().getShowAxes();
//    templateItem.setText(Translate.text(wasShown ? "menu.showCoordinateAxes" : "menu.hideCoordinateAxes"));
    for(int i = 0; i < theView.length; ++i)
      theView[i].setShowAxes(!wasShown);

    updateMenus();
    updateImage();
  }
  
  /** Toggle whether the template is shown. */
  
  void showTemplateCommand()
  {
    boolean wasShown = getCurrentView().getTemplateShown();
//    templateItem.setText(Translate.text(wasShown ? "menu.showTemplate" : "menu.hideTemplate"));
    getCurrentView().setShowTemplate(!wasShown);

    updateMenus();
    updateImage();
  }
  
  /** Allow the user to set the template image. */

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

  /** Delete the select joint from the skeleton. */

  public void deleteJointCommand()
  {
    Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    Skeleton s = theMesh.getSkeleton();

    if (s == null)
      return;
    Joint j = s.getJoint(getCurrentView().getSelectedJoint());
    if (j == null)
      return;
    String options[] = new String [] {Translate.text("Yes"), Translate.text("No")};
    BStandardDialog dlg = new BStandardDialog("", Translate.text(j.children.length == 0 ? "deleteBone" : "deleteBoneAndChildren", j.name), BStandardDialog.QUESTION);
    if (dlg.showOptionDialog(this, options, options[1]) == 1)
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_SKELETON, new Object [] {theMesh.getSkeleton(), theMesh.getSkeleton().duplicate()}));
    s.deleteJoint(getCurrentView().getSelectedJoint());
    getCurrentView().setSelectedJoint(j.parent == null ? -1 : j.parent.id);
    object3DChangedDuringEditor();  // TODO(MB) ?
    updateMenus();
  }

  /** Allow the user to set the parent of the selected joint. */
  
  public void setJointParentCommand()
  {
    MeshViewer theView = getCurrentView();
    Mesh theMesh = (Mesh) theView.getObject().object;
    Skeleton s = theMesh.getSkeleton();
    
    if (s == null)
      return;
    Joint j = s.getJoint(theView.getSelectedJoint());
    if (j == null)
      return;
    
    // Make a list of all joints which are possibilities to be the parent.
    
    Joint joint[] = s.getJoints();
    boolean isChild[] = new boolean [joint.length];
    markChildJoints(s, j, isChild);
    Vector options = new Vector();
    for (int i = 0; i < isChild.length; i++)
      if (!isChild[i])
        options.addElement(joint[i]);
    
    // Display a window for the user to select the parent joint.
    
    BList ls = new BList();
    ls.setMultipleSelectionEnabled(false);
    ls.add("("+Translate.text("None")+")");
    ls.setSelected(0, true);
    for (int i = 0; i < options.size(); i++)
    {
      ls.add(((Joint) options.elementAt(i)).name);
      if (options.elementAt(i) == j.parent)
        ls.setSelected(i+1, true);
    }
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("selectParentBone", j.name),
        new Widget [] {UIUtilities.createScrollingList(ls)}, new String [] {null});
    if (!dlg.clickedOk() || ls.getSelectedIndex() == -1)
      return;
    
    // Set the parent.
    
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_SKELETON, new Object [] {theMesh.getSkeleton(), theMesh.getSkeleton().duplicate()}));
    Joint oldParent = j.parent;
    if (ls.getSelectedIndex() == 0)
      s.setJointParent(j, null);
    else
      s.setJointParent(j, (Joint) options.elementAt(ls.getSelectedIndex()-1));
    
    // Adjust the coordinate system.
    
    if (j.parent != null)
    {
      Vec3 oldZdir = j.coords.getZDirection();
      Vec3 oldYdir = j.coords.getUpDirection();
      Vec3 oldXdir = oldYdir.cross(oldZdir);
      Vec3 xdir, ydir, zdir;
      zdir = j.coords.getOrigin().minus(j.parent.coords.getOrigin());
      j.length.pos = zdir.length();
      zdir.normalize();
      if (Math.abs(oldXdir.dot(zdir)) < Math.abs(oldYdir.dot(zdir)))
      {
        xdir = oldXdir.minus(zdir.times(oldXdir.dot(zdir)));
        xdir.normalize();
        ydir = zdir.cross(xdir);
      }
      else
      {
        ydir = oldYdir.minus(zdir.times(oldYdir.dot(zdir)));
        ydir.normalize();
        xdir = ydir.cross(zdir);
      }
      j.coords.setOrientation(zdir, ydir);
      j.calcAnglesFromCoords(false);
      for (int i = 0; i < j.children.length; i++)
        j.children[i].calcAnglesFromCoords(false);
    }
    else
      j.calcAnglesFromCoords(false);
    updateImage();
    updateMenus();
  }
  
  /** This is called by setJointParentCommand().  It identifies joints which are children of the
      specified one. */
  
  private void markChildJoints(Skeleton s, Joint j, boolean isChild[])
  {
    isChild[s.findJointIndex(j.id)] = true;
    for (int i = 0; i < j.children.length; i++)
      markChildJoints(s, j.children[i], isChild);
  }
  
  /** Allow the user to edit the selected joint. */

  public void editJointCommand()
  {
    Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    Skeleton s = theMesh.getSkeleton();

    if (s == null)
      return;
    Joint j = s.getJoint(getCurrentView().getSelectedJoint());
    if (j == null)
      return;
    new JointEditorDialog(this, getCurrentView(), j.id);
    updateImage();
    updateMenus();
  }

  /** Present a window for binding the selected vertices to the skeleton. */

  public void bindSkeletonCommand()
  {
    Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    Skeleton s = theMesh.getSkeleton();
    if (s == null)
      return;

    // Find the selected vertices.

    int i, j, selected[] = getCurrentView().getSelectionDistance();
    for (j = 0; j < selected.length && selected[j] != 0; j++);
    if (j == selected.length)
      return;

    // Prompt the user.

    ValueSlider blendSlider = new ValueSlider(0.0, 1.0, 100, 0.5);
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("bindPointsToSkeleton"),
      new Widget [] {blendSlider}, new String [] {Translate.text("ikWeightBlending")});
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    double blend = blendSlider.getValue();

    // Find the position and axis vectors for each joint.

    Joint joint[] = s.getJoints();
    Vec3 pos[] = new Vec3 [joint.length], axis[] = new Vec3 [joint.length];
    for (i = 0; i < joint.length; i++)
    {
      pos[i] = joint[i].coords.getOrigin();
      if (joint[i].parent == null || joint[i].length.pos == 0.0)
        continue;
      axis[i] = joint[i].coords.getZDirection();
      axis[i] = axis[i].times(1.0/axis[i].length());
    }

    // Loop over vertices and decide which joint to bind each one to.

    MeshVertex vert[] = theMesh.getVertices();
    double dist[] = new double [joint.length];
    for (i = 0; i < selected.length; i++)
    {
      if (selected[i] != 0)
        continue;
      int nearest = -1;
      
      // Find which bone it is nearest to.
      
      for (j = 0; j < joint.length; j++)
      {
        dist[j] = distToBone(vert[i].r, joint[j], axis[j]);
        if (nearest == -1 || dist[j] < dist[nearest])
          nearest = j;
      }
      if (nearest == -1)
        continue;
      
      // Find the secondary bone.
      
      int second = -1;
      if (joint[nearest].parent != null)
        second = s.findJointIndex(joint[nearest].parent.id);
      for (j = 0; j < joint[nearest].children.length; j++)
      {
        int k = s.findJointIndex(joint[nearest].children[j].id);
        if (k != -1 && (second == -1 || dist[k] < dist[second]))
          second = k;
      }

      // Select the binding parameters.
      
      if (second == -1)
      {
        vert[i].ikJoint = joint[nearest].id;
        vert[i].ikWeight = 1.0;
      }
      else if (joint[nearest].parent != null && joint[second].id == joint[nearest].parent.id)
      {
        vert[i].ikJoint = joint[nearest].id;
        double ratio = dist[nearest]/dist[second];
        if (ratio <= 1.0-blend)
          vert[i].ikWeight = 1.0;
        else
          vert[i].ikWeight = 0.5+0.5*(1.0-ratio)/blend;
      }
      else
      {
        double ratio = dist[nearest]/dist[second];
        if (ratio <= 1.0-blend)
          {
            vert[i].ikJoint = joint[nearest].id;
            vert[i].ikWeight = 1.0;
          }
        else
          {
            vert[i].ikJoint = joint[second].id;
            vert[i].ikWeight = 0.5-0.5*(1.0-ratio)/blend;
          }
      }
      vert[i].ikWeight = 0.001*Math.round(vert[i].ikWeight*1000.0);
    }
    object3DChangedDuringEditor();
  }

  /* Calculate the distance between a vertex and a bone. */

  private double distToBone(Vec3 v, Joint j, Vec3 axis)
  {
    Vec3 end = j.coords.getOrigin();
    if (axis == null)
      return end.distance(v);
    Vec3 base = j.parent.coords.getOrigin();
    Vec3 diff = v.minus(base);
    double dot = diff.dot(axis);
    if (dot < 0.0)
      return base.distance(v);
    if (dot > j.length.pos)
      return end.distance(v);
    diff.subtract(axis.times(dot));
    return diff.length();
  }

  /** Display a window for importing a skeleton from another object. */

  protected void importSkeletonCommand()
  {
    final TreeList tree = new TreeList(this);
    tree.setPreferredSize(new java.awt.Dimension(130, 100));
    tree.setAllowMultiple(false);
    tree.setUpdateEnabled(false);
    Scene theScene = getScene();
    class TreeElem extends ObjectTreeElement
    {
      public TreeElem(ObjectInfo info, TreeElement parent, TreeList tree)
      {
        super(info, parent, tree, false);
        selectable = (info != getCurrentView().thisObjectInScene && info.getSkeleton() != null);
        for (int i = 0; i < info.children.length; i++)
          children.addElement(new TreeElem(info.children[i], this, tree));
      }
      public boolean isGray()
      {
        return !selectable;
      }
      public boolean canAcceptAsParent(TreeElement el)
      {
        return false;
      }
    };
    for (int i = 0; i < theScene.getNumObjects(); i++)
    {
      ObjectInfo info = theScene.getObject(i);
      if (info.parent == null)
        tree.addElement(new TreeElem(info, null, tree));
    }
    tree.setUpdateEnabled(true);
    tree.setBackground(java.awt.Color.white);
    BScrollPane sp = new BScrollPane(tree, BScrollPane.SCROLLBAR_ALWAYS, BScrollPane.SCROLLBAR_ALWAYS);
    sp.getVerticalScrollBar().setUnitIncrement(10);
    sp.setForceWidth(true);
    sp.setForceHeight(true);
    ComponentsDialog dlg = new ComponentsDialog(this, "selectImportSkeleton", new Widget [] {sp}, new String [] {null});
    if (!dlg.clickedOk() || tree.getSelectedObjects().length == 0)
      return;
    Mesh theMesh = (Mesh) getCurrentView().getObject().object;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_SKELETON, new Object [] {theMesh.getSkeleton(), theMesh.getSkeleton().duplicate()}));
    ObjectInfo info = (ObjectInfo) tree.getSelectedObjects()[0];
    theMesh.getSkeleton().addAllJoints(info.object.getSkeleton());
    updateImage();
    updateMenus();
  }

  /** Set the grid options for the current window. */

  public void setGridCommand()
  {
    ValueField spaceField = new ValueField(getCurrentView().gridSpacing, ValueField.POSITIVE);
    ValueField divField = new ValueField(getCurrentView().gridSubdivisions, ValueField.POSITIVE+ValueField.INTEGER);
    BCheckBox showBox = new BCheckBox(Translate.text("showGrid"), getCurrentView().showGrid);
    BCheckBox snapBox = new BCheckBox(Translate.text("snapToGrid"), getCurrentView().snapToGrid);
    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;
    for(int i = 0; i < theView.length; ++i)
    {
      theView[i].gridSpacing = spaceField.getValue();
      theView[i].gridSubdivisions = (int) divField.getValue();
      theView[i].showGrid = showBox.getState();
      theView[i].snapToGrid = snapBox.getState();
      theView[i].setGrid(spaceField.getValue(), (int) divField.getValue(), showBox.getState(), snapBox.getState());
    }
    updateImage();
  }

  /** Propagate the settings of the current view to all other views. */
  public void setViewSettingsForAllViewsCommand()
  {
    MeshViewer cv = getCurrentView();
    for(int i = 0; i < theView.length; ++i)
    {
      if (cv == theView[i])
        continue;

      theView[i].copyViewSettings(cv);
      theView[i].updateImage();
      theView[i].repaint();
    }
  }

  /** Given a list of deltas which will be added to the selected vertices, calculate the
     corresponding deltas for the unselected vertices according to the mesh tension. */

  public abstract void adjustDeltas(Vec3 delta[]);
  
}