/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: CircuitChangeJobs.java
 *
 * Copyright (c) 2006, Static Free Software. All rights reserved.
 *
 * Electric(tm) 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 3 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.tool.user;

import com.sun.electric.database.hierarchy.BatchChanges;
import static com.sun.electric.util.collections.ArrayIterator.i2i;

import com.sun.electric.database.EditingPreferences;
import com.sun.electric.database.IdMapper;
import com.sun.electric.database.constraint.Layout;
import com.sun.electric.database.geometry.EPoint;
import com.sun.electric.database.geometry.ERectangle;
import com.sun.electric.database.geometry.Poly;
import com.sun.electric.database.geometry.PolyBase;
import com.sun.electric.database.geometry.PolyMerge;
import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.hierarchy.Cell.CellGroup;
import com.sun.electric.database.hierarchy.EDatabase;
import com.sun.electric.database.hierarchy.Export;
import com.sun.electric.database.hierarchy.Library;
import com.sun.electric.database.network.Netlist;
import com.sun.electric.database.network.Network;
import com.sun.electric.database.prototype.NodeProto;
import com.sun.electric.database.prototype.PortProto;
import com.sun.electric.database.text.Name;
import com.sun.electric.database.text.TextUtils;
import com.sun.electric.database.topology.ArcInst;
import com.sun.electric.database.topology.Connection;
import com.sun.electric.database.topology.Geometric;
import com.sun.electric.database.topology.NodeInst;
import com.sun.electric.database.topology.PortInst;
import com.sun.electric.database.variable.CodeExpression;
import com.sun.electric.database.variable.DisplayedText;
import com.sun.electric.database.variable.EditWindow_;
import com.sun.electric.database.variable.ElectricObject;
import com.sun.electric.database.variable.TextDescriptor;
import com.sun.electric.database.variable.UserInterface;
import com.sun.electric.database.variable.Variable;
import com.sun.electric.technology.ArcProto;
import com.sun.electric.technology.Layer;
import com.sun.electric.technology.PrimitiveNode;
import com.sun.electric.technology.Technology;
import com.sun.electric.technology.technologies.Artwork;
import com.sun.electric.technology.technologies.Generic;
import com.sun.electric.technology.technologies.Schematics;
import com.sun.electric.tool.Job;
import com.sun.electric.tool.JobException;
import com.sun.electric.tool.Tool;
import com.sun.electric.tool.io.FileType;
import com.sun.electric.tool.io.input.LibraryFiles;
import com.sun.electric.tool.user.dialogs.OpenFile;
import com.sun.electric.tool.user.ui.EditWindow;
import com.sun.electric.tool.user.ui.SizeListener;
import com.sun.electric.tool.user.ui.StatusBar;
import com.sun.electric.tool.user.ui.TopLevel;
import com.sun.electric.util.math.DBMath;
import com.sun.electric.util.math.ECoord;
import com.sun.electric.util.math.EDimension;
import com.sun.electric.util.math.FixpTransform;
import com.sun.electric.util.math.GenMath;
import com.sun.electric.util.math.Orientation;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.JOptionPane;

/**
 * Class for Jobs that make changes to the circuit.
 */
public class CircuitChangeJobs
{
	// constructor, never used
	CircuitChangeJobs() {}

	/****************************** NODE TRANSFORMATION ******************************/

	public static class RotateSelected extends Job
	{
		private Cell cell;
		private int amount;
		private boolean mirror;
		private boolean mirrorH;
		private List<Geometric> highs;

		/**
		 * @param cell
		 * @param highs the highlighted objects (list of highlights)
		 * @param amount angle in tenth degrees to rotate
		 * @param mirror whether or not to mirror. if true, amount is ignored, and mirrorH is used.
		 * @param mirrorH if true, mirror horizontally (flip over X-axis), otherwise mirror
		 * vertically (flip over Y-axis). Ignored if mirror is false.
		 */
		public RotateSelected(Cell cell, List<Geometric> highs, int amount, boolean mirror, boolean mirrorH)
		{
			super("Rotate selected objects", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.amount = amount;
			this.mirror = mirror;
			this.mirrorH = mirrorH;
			this.highs = highs;
			startJob();
		}

		public boolean doIt() throws JobException
		{
			// disallow rotating if lock is on
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			// remove first if center-facet is in the design
			// If not the only one -> remove it from the list
			// since rotation gets wrong.
			if (highs.size() > 1)
			{
				// at least one more than center-facet
				for(Geometric geom : highs)
				{
					if (!(geom instanceof NodeInst)) continue;
					if (Generic.isCellCenter((NodeInst)geom))
					{
						highs.remove(geom);
						break;
					}
				}
			}
			
			// figure out which nodes get rotated/mirrored
			Set<Geometric> markObj = new HashSet<Geometric>();
			int nicount = 0;
			NodeInst theNi = null;
			Rectangle2D selectedBounds = new Rectangle2D.Double();

            // Add arc if both pins are in the list to avoid weird changes by the constraints system
            for (Iterator<ArcInst> it = cell.getArcs(); it.hasNext();)
            {
                ArcInst ai = it.next();
                PortInst tail = ai.getTailPortInst();
                PortInst head = ai.getHeadPortInst();

                if (highs.contains(head) && highs.contains(tail))
                    highs.add(ai); // better to add the arc here
            }

            for(Geometric geom : highs)
			{
				if (!(geom instanceof NodeInst)) continue;
                NodeInst ni = (NodeInst)geom;

//                if (Generic.isSpecialGenericNode(ni))
            	if (Generic.isCellCenter(ni))
                    continue; // like center

				if (cantEdit(cell, ni, true, false, true) != 0)
				{
					return false;
				}
				markObj.add(ni);
				if (nicount == 0)
				{
					selectedBounds.setRect(ni.getBounds());
				} else
				{
					Rectangle2D.union(selectedBounds, ni.getBounds(), selectedBounds);
				}
				theNi = ni;
				nicount++;
			}

			// must be at least 1 node
			if (nicount <= 0)
			{
				System.out.println("Must select at least 1 node for rotation");
				return false;
			}

			// if multiple nodes, find the center one
			if (nicount > 1)
			{
				Point2D center = new Point2D.Double(selectedBounds.getCenterX(), selectedBounds.getCenterY());
				theNi = null;
				double bestdist = Integer.MAX_VALUE;
				for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
				{
					NodeInst ni = it.next();
					if (!markObj.contains(ni)) continue;
					double dist = center.distance(ni.getTrueCenter());
					if (theNi == null || dist < bestdist)
					{
						theNi = ni;
						bestdist = dist;
					}
				}
			}

			// see which nodes already connect to the main rotation/mirror node (theNi)
			markObj.clear();
			for(Geometric geom : highs)
			{
				if (!(geom instanceof ArcInst)) continue;
				ArcInst ai = (ArcInst)geom;
				markObj.add(ai);
			}
			spreadRotateConnection(theNi, markObj);

			// now make sure that it is all connected
			// highs should not contain the cell center
			for(Geometric geom : highs)
			{
				if (!(geom instanceof NodeInst)) continue;
				NodeInst ni = (NodeInst)geom;
				spreadRotateConnection(ni, markObj);
			}

			// do the rotation/mirror
			Orientation dOrient;
			if (mirror)
			{
				// do mirroring
				dOrient = mirrorH ? Orientation.Y : Orientation.X;
			} else
			{
				// do rotation
				dOrient = Orientation.fromAngle(amount);
			}
			FixpTransform trans = dOrient.rotateAbout(theNi.getAnchorCenter());

			Point2D.Double tmpPt1 = new Point2D.Double(), tmpPt2 = new Point2D.Double();

			// Rotate nodes in markObj
			for (Geometric geom : markObj)
			{
				if (!(geom instanceof NodeInst)) continue;
				NodeInst ni = (NodeInst)geom;
				boolean wasMirroredInX = ni.isMirroredAboutXAxis();
				boolean wasMirroredInY = ni.isMirroredAboutYAxis();
				trans.transform(ni.getAnchorCenter(), tmpPt1);
				ni.rotate(dOrient);
				ni.move(tmpPt1.getX() - ni.getAnchorCenterX(), tmpPt1.getY() - ni.getAnchorCenterY());
				rotateNodeText(ni, dOrient, wasMirroredInX, wasMirroredInY);
			}

			// Rotate arcs in markObj
			for (Geometric geom : markObj)
			{
				if (!(geom instanceof ArcInst)) continue;
				ArcInst ai = (ArcInst)geom;
				if (markObj.contains(ai.getHeadPortInst().getNodeInst()))
					trans.transform(ai.getHeadLocation(), tmpPt1);
				else
					tmpPt1.setLocation(ai.getHeadLocation());
				if (markObj.contains(ai.getTailPortInst().getNodeInst()))
					trans.transform(ai.getTailLocation(), tmpPt2);
				else
					tmpPt2.setLocation(ai.getTailLocation());
				ai.modify(tmpPt1.getX() - ai.getHeadLocation().getX(), tmpPt1.getY() - ai.getHeadLocation().getY(),
					tmpPt2.getX() - ai.getTailLocation().getX(), tmpPt2.getY() - ai.getTailLocation().getY());
				rotateArcText(ai, dOrient, mirror);
			}

			return true;
		}
	}

	/**
	 * Class to rotate selected text in a Job.
	 */
	public static class RotateText extends Job
	{
		private Cell cell;
        private int amount;
		private boolean mirror;
		private boolean mirrorH;
		private List<ElectricObject> objs;
		private List<Variable.Key> keys;

		/**
		 * @param cell the Cell in which to make the change.
		 * @param objs the objects (nodes, arcs, exports) that hold text to rotate.
		 * @param keys the keys on the nodes/arcs for the text to rotate.
		 * @param amount angle in tenth degrees to rotate (ignored for now).
		 */
		public RotateText(Cell cell, List<ElectricObject> objs, List<Variable.Key> keys, int amount,
                          boolean mirror, boolean mirrorH)
		{
			super("Rotate selected text", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.objs = objs;
			this.keys = keys;
            this.amount = amount;
            this.mirror = mirror;
            this.mirrorH = mirrorH;
			startJob();
		}

		public boolean doIt() throws JobException
		{
			// disallow rotating if lock is on
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			for(int i=0; i<objs.size(); i++)
			{
				ElectricObject obj = objs.get(i);
				Variable.Key key = keys.get(i);
				TextDescriptor td = obj.getTextDescriptor(key);
				TextDescriptor.Rotation oldR = td.getRotation();
                if (amount != 0)
                    if (oldR == TextDescriptor.Rotation.ROT0) oldR = TextDescriptor.Rotation.ROT90; else
                        if (oldR == TextDescriptor.Rotation.ROT90) oldR = TextDescriptor.Rotation.ROT180; else
                            if (oldR == TextDescriptor.Rotation.ROT180) oldR = TextDescriptor.Rotation.ROT270; else
                                oldR = TextDescriptor.Rotation.ROT0;
				td = td.withRotation(oldR);

                Poly.Type oldType = td.getPos().getPolyType();
                if (mirror && !mirrorH)
                    td = td.withPos(TextDescriptor.Position.getPosition(oldType.cycleTextAnchorHoriz()));
                else if (mirror && mirrorH)
                    td = td.withPos(TextDescriptor.Position.getPosition(oldType.cycleTextAnchorVert()));

				obj.setTextDescriptor(key, td);
			}
			return true;
		}
	}

	/**
	 * Method to rotate the text on an arc after it has been rotated/flipped.
	 * @param ai the ArcInst that was modified
	 * @param dOrient the change in Orientation.
	 * @param mirror true if the arc was mirrored before the change.
	 */
	public static void rotateArcText(ArcInst ai, Orientation dOrient, boolean mirror)
	{
		if (Poly.NEWTEXTTREATMENT)
		{
			// rotate any text on the arc
			rotateText(ai, ArcInst.ARC_NAME, dOrient, !mirror);
			for(Iterator<Variable> it = ai.getParametersAndVariables(); it.hasNext(); )
			{
				Variable var = it.next();
				rotateText(ai, var.getKey(), dOrient, true);
			}
		}
	}

	/**
	 * Method to rotate the text on a node after the node has been rotated/flipped.
	 * @param ni the NodeInst that was modified
	 * @param dOrient the change in Orientation.
	 * @param wasMirroredInX true if the node was mirrored in X before the change.
	 * @param wasMirroredInY true if the node was mirrored in Y before the change.
	 */
	public static void rotateNodeText(NodeInst ni, Orientation dOrient, boolean wasMirroredInX, boolean wasMirroredInY)
	{
		if (Poly.NEWTEXTTREATMENT)
		{
			// rotate any text on the node
			rotateText(ni, NodeInst.NODE_NAME, dOrient, wasMirroredInX==wasMirroredInY);
			for(Iterator<Variable> it = ni.getParametersAndVariables(); it.hasNext(); )
			{
				Variable var = it.next();
				rotateText(ni, var.getKey(), dOrient, wasMirroredInX==wasMirroredInY);
			}

			// rotate any exports on the node
			for(Iterator<Export> it = ni.getExports(); it.hasNext(); )
			{
				Export e = it.next();
				rotateText(e, Export.EXPORT_NAME, dOrient, wasMirroredInX==wasMirroredInY);
			}
		}
	}

	/**
	 * Method to rotate text on an object.
	 * @param eObj the ElectricObject on which the text resides.
	 * @param key the Key for the text on the object.
	 * @param dOrient the amount of rotation.
	 * @param wasSameMirror true if the L-R mirroring was the same as the U-D mirroring.
	 */
	private static void rotateText(ElectricObject eObj, Variable.Key key, Orientation dOrient, boolean wasSameMirror)
	{
		TextDescriptor td = eObj.getTextDescriptor(key);
		Poly.Type oldType = td.getPos().getPolyType();
		if (oldType == Poly.Type.TEXTCENT || oldType == Poly.Type.TEXTBOX) return;

		FixpTransform dTrans = dOrient.pureRotate();
		Poly.Type newType = oldType;
		if (dOrient == Orientation.X)
		{
			// mirror X (left-right)
			oldType = oldType.rotateTextAnchorIn(td.getRotation());
			if (oldType == Poly.Type.TEXTLEFT) oldType = Poly.Type.TEXTRIGHT; else
			if (oldType == Poly.Type.TEXTRIGHT) oldType = Poly.Type.TEXTLEFT; else
			if (oldType == Poly.Type.TEXTBOTLEFT) oldType = Poly.Type.TEXTBOTRIGHT; else
			if (oldType == Poly.Type.TEXTBOTRIGHT) oldType = Poly.Type.TEXTBOTLEFT; else
			if (oldType == Poly.Type.TEXTTOPLEFT) oldType = Poly.Type.TEXTTOPRIGHT; else
			if (oldType == Poly.Type.TEXTTOPRIGHT) oldType = Poly.Type.TEXTTOPLEFT;
			oldType = oldType.rotateTextAnchorOut(td.getRotation());
			newType = oldType;
		} else if (dOrient == Orientation.Y)
		{
			// mirror Y (top-bottom)
			oldType = oldType.rotateTextAnchorIn(td.getRotation());
			if (oldType == Poly.Type.TEXTBOT) oldType = Poly.Type.TEXTTOP; else
			if (oldType == Poly.Type.TEXTTOP) oldType = Poly.Type.TEXTBOT; else
			if (oldType == Poly.Type.TEXTBOTLEFT) oldType = Poly.Type.TEXTTOPLEFT; else
			if (oldType == Poly.Type.TEXTTOPLEFT) oldType = Poly.Type.TEXTBOTLEFT; else
			if (oldType == Poly.Type.TEXTBOTRIGHT) oldType = Poly.Type.TEXTTOPRIGHT; else
			if (oldType == Poly.Type.TEXTTOPRIGHT) oldType = Poly.Type.TEXTBOTRIGHT;
			oldType = oldType.rotateTextAnchorOut(td.getRotation());
			newType = oldType;
		} else
		{
			// determine change in angle because of node rotation
			int origAngle = oldType.getTextAngle();
//			if (!wasSameMirror && ((origAngle%1800) == 0 || (origAngle%1800) == 1350))
//				origAngle += 1800;
			Point2D pt = new Point2D.Double(100, 0);
			dTrans.transform(pt, pt);
			int xAngle = GenMath.figureAngle(new Point2D.Double(0, 0), pt);
			int angle = (origAngle + xAngle) % 3600;
			newType = Poly.Type.getTextTypeFromAngle(angle);
		}
		TextDescriptor.Position newPos = TextDescriptor.Position.getPosition(newType);
		td = td.withPos(newPos);
		eObj.setTextDescriptor(key, td);

		// if an arc, rotate the offset point
		if (eObj instanceof ArcInst)
		{
			Point2D pt = new Point2D.Double(td.getXOff(), td.getYOff());
			dTrans.transform(pt, pt);
			eObj.setTextDescriptor(key, td.withOff(pt.getX(), pt.getY()));
		}
	}

	/**
	 * Helper method for rotation to mark selected nodes that need not be
	 * connected with an invisible arc.
	 */
	private static void spreadRotateConnection(NodeInst theNi, Set<Geometric> markObj)
	{
		if (markObj.contains(theNi)) return;
		markObj.add(theNi);
		for(Iterator<Connection> it = theNi.getConnections(); it.hasNext(); )
		{
			Connection con = it.next();
			ArcInst ai = con.getArc();
			if (!markObj.contains(ai)) continue;
			int otherEnd = 1 - con.getEndIndex();
			NodeInst ni = ai.getPortInst(otherEnd).getNodeInst();
			if (markObj.contains(ni)) continue;
			markObj.add(ni);
			spreadRotateConnection(ni, markObj);
		}
	}

	/****************************** NODE ALIGNMENT ******************************/

	/**
	 * This class implement the command to align objects to the grid.
	 */
	public static class AlignObjects extends Job
	{
		private List<Geometric> list;			// list of highlighted objects to align
		private EDimension alignment;

		public AlignObjects(List<Geometric> highs, EDimension alignment)
		{
			super("Align Objects", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.list = highs;
			this.alignment = alignment;
			startJob();
		}

        @Override
		public boolean doIt() throws JobException
		{
//            EditingPreferences ep = getEditingPreferences();
			// first adjust the nodes
			int adjustedNodes = 0;
			for(Geometric geom : list)
			{
				if (!(geom instanceof NodeInst)) continue;
				NodeInst ni = (NodeInst)geom;

				Point2D [] points = ni.getTrace();
				if (points != null)
				{
					FixpTransform transOut = ni.pureRotateOut();
					Point2D [] newPoints = new Point2D[points.length];
					boolean changed = false;
					for(int i=0; i<points.length; i++)
					{
						if (points[i] == null) continue;
						Point2D newPoint = new Point2D.Double(points[i].getX() + ni.getAnchorCenterX(), points[i].getY() + ni.getAnchorCenterY());
						transOut.transform(newPoint, newPoint);
						double oldX = newPoint.getX();
						double oldY = newPoint.getY();
						DBMath.gridAlign(newPoint, alignment);
						if (oldX != newPoint.getX() || oldY != newPoint.getY()) changed = true;
						newPoints[i] = newPoint;
					}
					if (changed)
					{
						adjustedNodes++;
						ni.setTrace(newPoints);
					}
					continue;
				}

				Point2D center = new Point2D.Double(ni.getAnchorCenterX(), ni.getAnchorCenterY());
				DBMath.gridAlign(center, alignment);
				double bodyXOffset = center.getX() - ni.getAnchorCenterX();
				double bodyYOffset = center.getY() - ni.getAnchorCenterY();

				double portXOffset = bodyXOffset;
				double portYOffset = bodyYOffset;
				boolean mixedportpos = false;
				boolean firstPort = true;
				for(Iterator<PortInst> pIt = ni.getPortInsts(); pIt.hasNext(); )
				{
					PortInst pi = pIt.next();
					Poly poly = pi.getPoly();
					Point2D portCenter = new Point2D.Double(poly.getCenterX(), poly.getCenterY());
					DBMath.gridAlign(portCenter, alignment);
					double pXO = portCenter.getX() - poly.getCenterX();
					double pYO = portCenter.getY() - poly.getCenterY();
					if (firstPort)
					{
						firstPort = false;
						portXOffset = pXO;   portYOffset = pYO;
					} else
					{
						if (portXOffset != pXO || portYOffset != pYO) mixedportpos = true;
					}
				}
				if (!mixedportpos)
				{
					bodyXOffset = portXOffset;   bodyYOffset = portYOffset;
				}

				// if a primitive has an offset, see if the node edges are aligned
				if (bodyXOffset != 0 || bodyYOffset != 0)
				{
					if (!ni.isCellInstance())
					{
						FixpTransform transr = ni.rotateOut();
						Technology tech = ni.getProto().getTechnology();
						Poly [] polyList = tech.getShapeOfNode(ni);
						for(int j=0; j<polyList.length; j++)
						{
							Poly poly = polyList[j];
							poly.transform(transr);
							Rectangle2D bounds = poly.getBox();
							if (bounds == null) continue;
							Point2D polyPoint1 = new Point2D.Double(bounds.getMinX(), bounds.getMinY());
							Point2D polyPoint2 = new Point2D.Double(bounds.getMaxX(), bounds.getMaxY());
							DBMath.gridAlign(polyPoint1, alignment);
							DBMath.gridAlign(polyPoint2, alignment);
							if (polyPoint1.getX() == bounds.getMinX() &&
								polyPoint2.getX() == bounds.getMaxX()) bodyXOffset = 0;
							if (polyPoint1.getY() == bounds.getMinY() &&
								polyPoint2.getY() == bounds.getMaxY()) bodyYOffset = 0;
							if (bodyXOffset == 0 && bodyYOffset == 0) break;
						}
					}
				}

				// move the node
				if (bodyXOffset != 0 || bodyYOffset != 0)
				{
					// turn off all constraints on arcs
					Map<ArcInst,Integer> constraints = new HashMap<ArcInst,Integer>();
					for(Iterator<Connection> aIt = ni.getConnections(); aIt.hasNext(); )
					{
						Connection con = aIt.next();
						ArcInst ai = con.getArc();
						int constr = 0;
						if (ai.isRigid()) constr |= 1;
						if (ai.isFixedAngle()) constr |= 2;
						constraints.put(ai, Integer.valueOf(constr));
					}
					ni.move(bodyXOffset, bodyYOffset);
					adjustedNodes++;

					// restore arc constraints
					for(Iterator<Connection> aIt = ni.getConnections(); aIt.hasNext(); )
					{
						Connection con = aIt.next();
						ArcInst ai = con.getArc();
						Integer constr = constraints.get(ai);
						if (constr == null) continue;
						if ((constr.intValue() & 1) != 0) ai.setRigid(true);
						if ((constr.intValue() & 2) != 0) ai.setFixedAngle(true);
					}
				}
			}

			// now adjust the arcs
			int adjustedArcs = 0;
			for(Geometric geom : list)
			{
				if (!(geom instanceof ArcInst)) continue;
				ArcInst ai = (ArcInst)geom;
				if (!ai.isLinked()) continue;

				Point2D origHead = ai.getHeadLocation();
				Point2D origTail = ai.getTailLocation();
				Point2D arcHead = new Point2D.Double(origHead.getX(), origHead.getY());
				Point2D arcTail = new Point2D.Double(origTail.getX(), origTail.getY());
				DBMath.gridAlign(arcHead, alignment);
				DBMath.gridAlign(arcTail, alignment);

                if ((arcHead.getX() == arcTail.getX()) && (arcHead.getY() == arcTail.getY())) {
                    // zero length arc, make sure angle is correct
                    int ang = ai.getDefinedAngle();
                    if (ang != 0 && ang != 900 && ang != 1800 && ang != 2700) {
                        ai.setAngle(0);
                        adjustedArcs++;
                    }
                }

                double headXOff = arcHead.getX() - origHead.getX();
				double headYOff = arcHead.getY() - origHead.getY();
				double tailXOff = arcTail.getX() - origTail.getX();
				double tailYOff = arcTail.getY() - origTail.getY();
				if (headXOff == 0 && tailXOff == 0 && headYOff == 0 && tailYOff == 0) continue;

				if (!ai.headStillInPort(arcHead, false))
				{
					if (!ai.headStillInPort(origHead, false)) continue;
					headXOff = headYOff = 0;
				}
				if (!ai.tailStillInPort(arcTail, false))
				{
					if (!ai.tailStillInPort(origTail, false)) continue;
					tailXOff = tailYOff = 0;
				}

				// make sure an arc does not change angle
				int ang = ai.getDefinedAngle();
				if (ang == 0 || ang == 1800)
				{
					// horizontal arc: both DY values must be the same
					if (headYOff != tailYOff) headYOff = tailYOff = 0;
				} else if (ang == 900 || ang == 2700)
				{
					// vertical arc: both DX values must be the same
					if (headXOff != tailXOff) headXOff = tailXOff = 0;
				}
				if (headXOff != 0 || tailXOff != 0 || headYOff != 0 || tailYOff != 0)
				{
					int constr = 0;
					if (ai.isRigid()) constr |= 1;
					if (ai.isFixedAngle()) constr |= 2;
					ai.setRigid(false);
					ai.setFixedAngle(false);

					ai.modify(headXOff, headYOff, tailXOff, tailYOff);
					adjustedArcs++;
					if ((constr & 1) != 0) ai.setRigid(true);
					if ((constr & 2) != 0) ai.setFixedAngle(true);
				}
			}

			// show results
			if (adjustedNodes == 0 && adjustedArcs == 0) System.out.println("No adjustments necessary"); else
				System.out.println("Adjusted " + adjustedNodes + " nodes and " + adjustedArcs + " arcs");
			return true;
		}
	}

	public static class AlignNodes extends Job
	{
		private NodeInst [] nis;
		private double [] dCX;
		private double [] dCY;

		public AlignNodes(NodeInst [] nis, double [] dCX, double [] dCY)
		{
			super("Align objects", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.nis = nis;
			this.dCX = dCX;
			this.dCY = dCY;
			startJob();
		}

		public boolean doIt() throws JobException
		{
			int numRemoved = 0;
			for(int i=0; i<nis.length; i++)
			{
				NodeInst ni = nis[i];
				int res = cantEdit(ni.getParent(), ni, true, false, true);
				if (res < 0) return false;
				if (res > 0)
				{
					numRemoved++;
					nis[i] = null;
				}
			}
			if (numRemoved > 0)
			{
				// make a smaller list
				int newSize = nis.length - numRemoved;
				if (newSize == 0) return true;
				NodeInst [] nnis = new NodeInst[newSize];
				double [] nCX = new double[newSize];
				double [] nCY = new double[newSize];
				int fill = 0;
				for(int i=0; i<nis.length; i++)
				{
					if (nis[i] == null) continue;
					nnis[fill] = nis[i];
					nCX[fill] = dCX[i];
					nCY[fill] = dCY[i];
					fill++;
				}
				nis = nnis;
				dCX = nCX;
				dCY = nCY;
			}

			NodeInst.modifyInstances(nis, dCX, dCY, null, null);
			return true;
		}
	}

	/****************************** ARC MODIFICATION ******************************/

	public enum ChangeArcEnum {
		RIGID("Rigid"),
		NONRIGID("Non-Rigid"),
		FIXEDANGLE("Fixed-Angle"),
		NONFIXEDANGLE("Not-Fixed-Angle"),
		DIRECTIONAL("Directional"),
		HEADEXTEND("extend the head end"),
		TAILEXTEND("extend the tail end");

		private String name;
		ChangeArcEnum(String n) { name = n; }
		public String toString() { return name; }
	}

	public static class ChangeArcProperties extends Job
	{
		private Cell cell;
		private ChangeArcEnum how;
		private List<ElectricObject> objList;
		private boolean repaintContents, repaintAny;

		public ChangeArcProperties(Cell cell, ChangeArcEnum how, List<Highlight> highlighted)
		{
			super("Align objects", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.how = how;
			this.objList = new ArrayList<ElectricObject>();

			for(Highlight h : highlighted)
			{
				if (!h.isHighlightEOBJ()) continue;
				objList.add(h.getElectricObject());
			}

			startJob();
		}

		public boolean doIt() throws JobException
		{
			// make sure changing arcs is allowed
			if (CircuitChangeJobs.cantEdit(cell, null, true, false, true) != 0) return false;

			int numSet = 0, numUnset = 0;
			for(ElectricObject eobj : objList)
			{
				if (eobj instanceof ArcInst)
				{
					ArcInst ai = (ArcInst)eobj;
					switch (how)
					{
						case RIGID:
							if (!ai.isRigid())
							{
								ai.setRigid(true);
								numSet++;
							}
							break;
						case NONRIGID:
							if (ai.isRigid())
							{
								ai.setRigid(false);
								numSet++;
							}
							break;
						case FIXEDANGLE:
							if (!ai.isFixedAngle())
							{
								ai.setFixedAngle(true);
								numSet++;
							}
							break;
						case NONFIXEDANGLE:
							if (ai.isFixedAngle())
							{
								ai.setFixedAngle(false);
								numSet++;
							}
							break;
						case DIRECTIONAL:		// toggle directionality
							if (ai.isHeadArrowed())
							{
								ai.setHeadArrowed(false);
								ai.setBodyArrowed(false);
								numUnset++;
							} else
							{
								ai.setHeadArrowed(true);
								ai.setBodyArrowed(true);
								numSet++;
							}
							break;
						case HEADEXTEND:		// end-extend the head
							if (ai.isHeadExtended())
							{
								ai.setHeadExtended(false);
								numUnset++;
							} else
							{
								ai.setHeadExtended(true);
								numSet++;
							}
							break;
						case TAILEXTEND:		// end-extend the tail
							if (ai.isTailExtended())
							{
								ai.setTailExtended(false);
								numUnset++;
							} else
							{
								ai.setTailExtended(true);
								numSet++;
							}
							break;
					}
				}
			}

			repaintAny = false;
			if (numSet == 0 && numUnset == 0) System.out.println("No changes were made"); else
			{
				String action = "";
				repaintAny = true;
				repaintContents = false;
				action = how.toString();

				switch (how)
				{
					case DIRECTIONAL:
					case HEADEXTEND:
					case TAILEXTEND:
						repaintContents = true;   break;
					default:
						break;
				}
				if (numUnset == 0) System.out.println("Made " + numSet + " arcs " + action); else
					if (numSet == 0) System.out.println("Made " + numUnset + " arcs not " + action); else
						System.out.println("Made " + numSet + " arcs " + action + "; and " + numUnset + " arcs not " + action);
			}
			fieldVariableChanged("repaintAny");
			fieldVariableChanged("repaintContents");
			return true;
		}

		public void terminateOK()
		{
			if (repaintAny)
			{
				if (repaintContents) EditWindow.repaintAllContents(); else
					EditWindow.repaintAll();
			}
		}
	}

	public static class ToggleNegationJob extends Job
	{
		private Cell cell;
		private List<ElectricObject> highlighted; // Can't use Highlight since it is not serializable
		private int numSet;

		public ToggleNegationJob(Cell cell, List<Highlight> highlighted)
		{
			super("Toggle negation", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.highlighted = new ArrayList<ElectricObject>();
			for(Highlight h : highlighted)
			{
				if (!h.isHighlightEOBJ()) continue;
				this.highlighted.add(h.getElectricObject());
			}
			startJob();
		}

		public boolean doIt() throws JobException
		{
			// make sure negation is allowed
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			numSet = 0;
			for(ElectricObject eobj : highlighted)
			{
				if (eobj instanceof PortInst)
				{
					PortInst pi = (PortInst)eobj;
					for(Iterator<Connection> cIt = pi.getConnections(); cIt.hasNext(); )
					{
						Connection con = cIt.next();
						con.setNegated(!con.isNegated());
					}
				}
				if (eobj instanceof ArcInst)
				{
					ArcInst ai = (ArcInst)eobj;
					for(int i=0; i<2; i++)
					{
						ai.setNegated(i, !ai.isNegated((i)));
					}
				}
			}
			fieldVariableChanged("numSet");
			if (numSet == 0) System.out.println("No ports negated"); else
			{
				System.out.println("Negated " + numSet + " ports");
			}
			return true;
		}

		public void terminateOK()
		{
			if (numSet != 0)
			{
				EditWindow.repaintAllContents();
			}
		}
	}

	public static class RipTheBus extends Job
	{
		private Cell cell;
		private List<ArcInst> list;

		public RipTheBus(Cell cell, List<ArcInst> list)
		{
			super("Rip Bus", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.list = list;
			startJob();
		}

        @Override
		public boolean doIt() throws JobException
		{
            EditingPreferences ep = getEditingPreferences();
			// make sure ripping arcs is allowed
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			for(ArcInst ai : list)
			{
				if (ai.getProto() != Schematics.tech().bus_arc) continue;
				Netlist netList = ai.getParent().getNetlist();
				if (netList == null)
				{
					System.out.println("Sorry, a deadlock aborted bus ripping (network information unavailable).  Please try again");
					break;
				}
				int busWidth = netList.getBusWidth(ai);
				String netName = netList.getNetworkName(ai);
				if (netName.length() == 0)
				{
					System.out.println("Bus " + ai.describe(true) + " has no name");
					continue;
				}

				// determine length of stub wires
				double stublen = (int)(ai.getLambdaLength() / 3 + 0.5);
				double lowXBus = 0, lowYBus = 0;
				int lowEnd = 1;
				double sepX = 0, sepY = 0;
				double lowX = 0, lowY = 0;

				// determine location of individual signals
				if (ai.getHeadLocation().getX() == ai.getTailLocation().getX())
				{
					lowX = ai.getHeadLocation().getX();
					if (lowX < ai.getParent().getBounds().getCenterX()) lowX += stublen; else
						lowX -= stublen;

					if (ai.getLocation(0).getY() < ai.getLocation(1).getY()) lowEnd = 0;
					lowY = (int)(ai.getLocation(lowEnd).getY());
					double highy = (int)(ai.getLocation(1-lowEnd).getY());
					if (highy-lowY >= busWidth-1)
					{
						// signals fit on grid
						sepY = (int)((highy-lowY) / (busWidth-1));
						lowY = (int)(((highy - lowY) - (sepY * (busWidth-1))) / 2 + lowY);
					} else
					{
						// signals don't fit: just make them even
						lowY = ai.getLocation(lowEnd).getY();
						highy = ai.getLocation(1-lowEnd).getY();
						sepY = (highy-lowY) / (busWidth-1);
					}
					lowXBus = ai.getTailLocation().getX();   lowYBus = lowY;
				} else if (ai.getTailLocation().getY() == ai.getHeadLocation().getY())
				{
					lowY = ai.getTailLocation().getY();
					if (lowY < ai.getParent().getBounds().getCenterY()) lowY += stublen; else
						lowY -= stublen;

					if (ai.getLocation(0).getX() < ai.getLocation(1).getX()) lowEnd = 0;
					lowX = (int)(ai.getLocation(lowEnd).getX());
					double highx = (int)(ai.getLocation(1-lowEnd).getX());
					if (highx-lowX >= busWidth-1)
					{
						// signals fit on grid
						sepX = (int)((highx-lowX) / (busWidth-1));
						lowX = (int)(((highx - lowX) - (sepX * (busWidth-1))) / 2 + lowX);
					} else
					{
						// signals don't fit: just make them even
						lowX = ai.getLocation(lowEnd).getX();
						highx = ai.getLocation(1-lowEnd).getX();
						sepX = (highx-lowX) / (busWidth-1);
					}
					lowXBus = lowX;   lowYBus = ai.getTailLocation().getY();
				} else
				{
					System.out.println("Bus " + ai.describe(true) + " must be horizontal or vertical to be ripped out");
					continue;
				}

				// copy names to a local array
				String [] localStrings = new String[busWidth];
				for(int i=0; i<busWidth; i++)
				{
					Network subNet = netList.getNetwork(ai, i);
					localStrings[i] = subNet.getName();
				}

				double sxw = Schematics.tech().wirePinNode.getDefWidth(ep);
				double syw = Schematics.tech().wirePinNode.getDefHeight(ep);
				double sxb = Schematics.tech().busPinNode.getDefWidth(ep);
				double syb = Schematics.tech().busPinNode.getDefHeight(ep);
				ArcProto apW = Schematics.tech().wire_arc;
				ArcProto apB = Schematics.tech().bus_arc;
				NodeInst niBLast = null;
				for(int i=0; i<busWidth; i++)
				{
					// make the wire pin
					NodeInst niw = NodeInst.makeInstance(Schematics.tech().wirePinNode, ep, new Point2D.Double(lowX, lowY), sxw, syw, ai.getParent());
					if (niw == null) break;

					// make the bus pin
					NodeInst nib = NodeInst.makeInstance(Schematics.tech().busPinNode, ep, new Point2D.Double(lowXBus, lowYBus), sxb, syb, ai.getParent());
					if (nib == null) break;

					// wire them
					PortInst head = niw.getOnlyPortInst();
					PortInst tail = nib.getOnlyPortInst();
					ArcInst aiw = ArcInst.makeInstance(apW, ep, head, tail);
					if (aiw == null) break;
					aiw.setName(localStrings[i], ep);

					// wire to the bus pin
					if (i == 0)
					{
						PortInst first = ai.getPortInst(lowEnd);
						aiw = ArcInst.makeInstance(apB, ep, first, tail);
					} else
					{
						PortInst first = niBLast.getOnlyPortInst();
						aiw = ArcInst.makeInstance(apB, ep, first, tail);
					}
					if (aiw == null) break;

					// advance to the next segment
					niBLast = nib;
					lowX += sepX;     lowY += sepY;
					lowXBus += sepX;  lowYBus += sepY;
				}

				// wire up the last segment
				PortInst head = niBLast.getOnlyPortInst();
				PortInst tail = ai.getPortInst(1-lowEnd);
				ArcInst aiw = ArcInst.makeInstance(apB, ep, head, tail);
				if (aiw == null) return false;
				aiw.setName(netName, ep);

				// remove original arc
				ai.kill();
			}
			return true;
		}
	}

	/****************************** DELETE SELECTED OBJECTS ******************************/

	public static class DeleteSelected extends Job
	{
		private Cell cell;
		private List<DisplayedText> highlightedText;
		private List<Geometric> highlighted;
		private boolean reconstructArcsAndExports;
		private List<Geometric> thingsToHighlight;

		public DeleteSelected(Cell cell, List<DisplayedText> highlightedText, List<Geometric> highlighted, boolean reconstructArcsAndExports)
		{
			super("Delete selected objects", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.highlightedText = highlightedText;
			this.highlighted = highlighted;
			this.reconstructArcsAndExports = reconstructArcsAndExports;
			startJob();
		}

        @Override
		public boolean doIt() throws JobException
		{
            EditingPreferences ep = getEditingPreferences();
			// make sure deletion is allowed
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			// delete the text
			int numNodeTextDeleted = 0, numArcTextDeleted = 0, numExportsDeleted = 0, numVariablesDeleted = 0;
			for(DisplayedText dt : highlightedText)
			{
				// deleting variable on object
				Variable.Key key = dt.getVariableKey();
				ElectricObject eobj = dt.getElectricObject();

                // delete any reference to this Object in SizeListener
                SizeListener.restorePreviousListener(eobj);

                if (key == NodeInst.NODE_NAME)
				{
					// deleting the name of a node
					NodeInst ni = (NodeInst)eobj;
					ni.setName(null);
					ni.move(0, 0);
					numNodeTextDeleted++;
				} else if (key == ArcInst.ARC_NAME)
				{
					// deleting the name of an arc
					ArcInst ai = (ArcInst)eobj;
					ai.setName(null, ep);
					ai.modify(0, 0, 0, 0);
					numArcTextDeleted++;
				} else if (key == Export.EXPORT_NAME)
				{
					// deleting the name of an export
					Export pp = (Export)eobj;
					int errCode = cantEdit(cell, pp.getOriginalPort().getNodeInst(), true, true, true);
					if (errCode < 0) return false;
					if (errCode > 0) continue;
					pp.kill();
					numExportsDeleted++;
				} else
				{
					// deleting a variable
					if (eobj.isParam(key)) {
						if (eobj instanceof Cell) {
							CellGroup cg = ((Cell)eobj).getCellGroup();
							if (cg != null) cg.delParam((Variable.AttrKey)key);
						} else if (eobj instanceof NodeInst)
							((NodeInst)eobj).delParameter(key);
					} else {
						eobj.delVar(key);
					}
					numVariablesDeleted++;
				}
			}
			if (numNodeTextDeleted != 0 || numArcTextDeleted != 0 || numExportsDeleted != 0 || numVariablesDeleted != 0)
			{
				String msg = "";
				if (numArcTextDeleted != 0)
				{
					msg = "Deleted";
					if (numNodeTextDeleted == 1) msg += " text on 1 Node"; else
						if (numNodeTextDeleted > 1) msg += " text on " + numNodeTextDeleted + " Nodes";
				}
				if (numArcTextDeleted != 0)
				{
					if (msg.length() == 0) msg = "Deleted"; else msg += ",";
					if (numArcTextDeleted == 1) msg += " text on 1 Arc"; else
						if (numArcTextDeleted > 1) msg += " text on " + numArcTextDeleted + " Arcs";
				}
				if (numExportsDeleted != 0)
				{
					if (msg.length() == 0) msg = "Deleted"; else msg += ",";
					if (numExportsDeleted == 1) msg += " 1 Export"; else
						if (numExportsDeleted > 1) msg += " " + numExportsDeleted + " Exports";
				}
				if (numVariablesDeleted != 0)
				{
					if (msg.length() == 0) msg = "Deleted"; else msg += ",";
					if (numVariablesDeleted == 1) msg += " 1 Variable"; else
						if (numVariablesDeleted > 1) msg += " " + numVariablesDeleted + " Variables";
				}
				System.out.println(msg);
			}
			if (cell != null)
			{
				thingsToHighlight = new ArrayList<Geometric>();
				Set<ElectricObject> stuffToHighlight = new HashSet<ElectricObject>();
				eraseObjectsInList(cell, highlighted, reconstructArcsAndExports, stuffToHighlight, ep);
				for(ElectricObject eObj : stuffToHighlight)
				{
					if (eObj instanceof ArcInst)
					{
						ArcInst ai = (ArcInst)eObj;
						thingsToHighlight.add(ai);
					} else if (eObj instanceof Export)
					{
						Export e = (Export)eObj;
						thingsToHighlight.add(e.getOriginalPort().getNodeInst());
					}
				}
				fieldVariableChanged("thingsToHighlight");
			}

			return true;
		}

        @Override
		public void terminateOK()
		{
			// remove highlighting, show only reconstructed objects
			UserInterface ui = Job.getUserInterface();
			EditWindow_ wnd = ui.getCurrentEditWindow_();
			if (wnd != null)
			{
				wnd.clearHighlighting();
				if (thingsToHighlight != null)
				{
					for(Geometric geom: thingsToHighlight)
						wnd.addElectricObject(geom, cell);
				}
				wnd.finishedHighlighting();
			}
		}
	}

	public static class DeleteSelectedGeometry extends Job
	{
		private Cell cell;
		private ERectangle bounds;

		public DeleteSelectedGeometry(Cell cell, Rectangle2D bounds)
		{
			super("Delete selected geometry", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.bounds = ERectangle.fromLambda(bounds);
			startJob();
		}

        @Override
		public boolean doIt() throws JobException
		{
            EditingPreferences ep = getEditingPreferences();
			// disallow erasing if lock is on
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			// grid the area
			double lX = Math.floor(bounds.getMinX());
			double hX = Math.ceil(bounds.getMaxX());
			double lY = Math.floor(bounds.getMinY());
			double hY = Math.ceil(bounds.getMaxY());

			// crop arcs that cross the area boundary
			List<ArcInst> arcsInCell = new ArrayList<ArcInst>();
			for(Iterator<ArcInst> aIt = cell.getArcs(); aIt.hasNext(); )
				arcsInCell.add(aIt.next());
			for(ArcInst ai : arcsInCell)
			{
				// if an end is inside, ignore
				Point2D headPt = ai.getHeadLocation();
				Point2D tailPt = ai.getTailLocation();

				// if length is zero, ignore
				if (tailPt.getX() == headPt.getX() &&
					tailPt.getY() == headPt.getY()) continue;

				// if the arc doesn't intersect the area, ignore
				double halfWidth = ai.getLambdaBaseWidth() / 2;
				double lXExt = lX - halfWidth;
				double hXExt = hX + halfWidth;
				double lYExt = lY - halfWidth;
				double hYExt = hY + halfWidth;
				Point2D tailPtAdj = new Point2D.Double(tailPt.getX(), tailPt.getY());
				Point2D headPtAdj = new Point2D.Double(headPt.getX(), headPt.getY());
				if (DBMath.clipLine(tailPtAdj, headPtAdj, lXExt, hXExt, lYExt, hYExt)) continue;
				if (tailPtAdj.distance(headPt) + headPtAdj.distance(tailPt) <
					headPtAdj.distance(headPt) + tailPtAdj.distance(tailPt))
				{
					Point2D swap = headPtAdj;
					headPtAdj = tailPtAdj;
					tailPtAdj = swap;
				}
				Name name = ai.getNameKey();
				String newName = null;
				if (!name.isTempname()) newName = name.toString();
				if (!tailPt.equals(tailPtAdj))
				{
					// create a pin at this point
					PrimitiveNode pin = ai.getProto().findPinProto();
					NodeInst ni = NodeInst.makeInstance(pin, ep, tailPtAdj, pin.getDefWidth(ep), pin.getDefHeight(ep), cell);
					if (ni == null)
					{
						System.out.println("Error creating pin for shortening of "+ai);
						continue;
					}

					ArcInst ai1 = ArcInst.makeInstanceBase(ai.getProto(), ep, ai.getLambdaBaseWidth(),
						ai.getTailPortInst(), ni.getOnlyPortInst(), ai.getTailLocation(),
							tailPtAdj, newName);
					if (ai1 == null)
					{
						System.out.println("Error shortening "+ai);
						continue;
					}
					newName = null;
					ai1.copyPropertiesFrom(ai);
				}
				if (!headPt.equals(headPtAdj))
				{
					// create a pin at this point
					PrimitiveNode pin = ai.getProto().findPinProto();
					NodeInst ni = NodeInst.makeInstance(pin, ep, headPtAdj, pin.getDefWidth(ep), pin.getDefHeight(ep), cell);
					if (ni == null)
					{
						System.out.println("Error creating pin for shortening of "+ai);
						continue;
					}

					ArcInst ai1 = ArcInst.makeInstanceBase(ai.getProto(), ep, ai.getLambdaBaseWidth(), ni.getOnlyPortInst(),
						ai.getHeadPortInst(), headPtAdj, ai.getHeadLocation(), newName);
					if (ai1 == null)
					{
						System.out.println("Error shortening "+ai);
						continue;
					}
					ai1.copyPropertiesFrom(ai);
				}

                // delete any reference to this Object in SizeListener
                SizeListener.restorePreviousListener(ai);

                ai.kill();
			}

			// now remove nodes in the area
			Set<NodeInst> nodesToDelete = new HashSet<NodeInst>();
			for(Iterator<NodeInst> nIt = cell.getNodes(); nIt.hasNext(); )
			{
				NodeInst ni = nIt.next();

				// special case for nodes with outline information: clip the outline
				if (!ni.isCellInstance())
				{
					if (ni.getProto().getFunction() == PrimitiveNode.Function.NODE)
					{
						// first see if it is completely ignored
						Rectangle2D pointBounds = ni.getBounds();
						if (pointBounds.getMinX() > hX || pointBounds.getMaxX() < lX ||
							pointBounds.getMinY() > hY || pointBounds.getMaxY() < lY) continue;

						// if it cannot be modified, stop
						int errorCode = cantEdit(cell, ni, true, false, true);
						if (errorCode < 0) return false;
						if (errorCode > 0) continue;

						// if it is completely covered, delete it
						if (pointBounds.getMinX() >= lX && pointBounds.getMaxX() <= hX &&
							pointBounds.getMinY() >= lY && pointBounds.getMaxY() <= hY)
						{
							nodesToDelete.add(ni);
							continue;
						}

						// crop it against the delete bounds
						Layer lay = ni.getProto().getTechnology().getLayer(0);
						PolyMerge merge = new PolyMerge();
						PolyBase poly;
						Point2D [] points = ni.getTrace();
						if (points == null) poly = new PolyBase(pointBounds); else
						{
							double cX = pointBounds.getCenterX();
							double cY = pointBounds.getCenterY();
							PolyBase.Point [] newPoints = new PolyBase.Point[points.length];
							for(int i=0; i<points.length; i++)
							{
								if (points[i] != null)
									newPoints[i] = PolyBase.fromLambda(points[i].getX()+cX, points[i].getY()+cY);
							}
							poly = new PolyBase(newPoints);
							poly.transform(ni.rotateOut());
						}
						merge.addPolygon(lay, poly);
						PolyBase polySub = new PolyBase((lX+hX)/2, (lY+hY)/2, hX-lX, hY-lY);
						merge.subtract(lay, polySub);
						List<PolyBase> resultingPolys = merge.getMergedPoints(lay, true);
						if (resultingPolys != null)
						{
							// find the largest polygon
							PolyBase largest = null;
							double largestSize = 0;
							for(PolyBase pb : resultingPolys)
							{
								double sz = pb.getArea();
								if (sz >= largestSize)
								{
									largestSize = sz;
									largest = pb;
								}
							}
							if (largest != null)
								ni.setTrace(largest.getPoints());
							continue;
						}
					}
				}

				// if the node is outside of the area, ignore it
				double cX = ni.getTrueCenterX();
				double cY = ni.getTrueCenterY();
				if (cX >= hX || cX <= lX || cY >= hY || cY <= lY) continue;

				// if it cannot be modified, stop
				int errorCode = cantEdit(cell, ni, true, false, true);
				if (errorCode < 0) return false;
				if (errorCode > 0) continue;
				nodesToDelete.add(ni);
                // delete any reference to this Object in SizeListener
                SizeListener.restorePreviousListener(ni);
            }

			// delete the nodes
			cell.killNodes(nodesToDelete);
			return true;
		}
	}

	public static class DeleteArcs extends Job
	{
		private Set<ArcInst> arcsToDelete;

		public DeleteArcs(Set<ArcInst> arcsToDelete)
		{
			super("Delete arcs", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.arcsToDelete = arcsToDelete;
			startJob();
		}

		public boolean doIt() throws JobException
		{
			for(ArcInst ai : arcsToDelete)
			{
				NodeInst h = ai.getHeadPortInst().getNodeInst();
				NodeInst t = ai.getTailPortInst().getNodeInst();
				ai.kill();

				// also delete freed pin nodes
				if (h.getProto().getFunction().isPin() &&
					!h.hasConnections() && !h.hasExports())
				{
					h.kill();
				}
				if (t.getProto().getFunction().isPin() &&
					!t.hasConnections() && !t.hasExports())
				{
					t.kill();
				}
			}
			System.out.println("Deleted " + arcsToDelete.size() + " arcs");
			return true;
		}
	}

	/****************************** DELETE OBJECTS IN A LIST ******************************/

	/**
	 * Method to delete all of the Geometrics in a list.
	 * @param cell the cell with the objects to be deleted.
	 * @param list a List of Geometric or Highlight objects to be deleted.
	 * @param reconstructArcsAndExports true to reconstruct arcs to deleted cell instances.
	 * @param stuffToHighlight a set of objects to select (arcs and exports) if reconstruction is done.
     * @param ep EditingPreferences
	 */
	public static void eraseObjectsInList(Cell cell, List<Geometric> list, boolean reconstructArcsAndExports,
		Set<ElectricObject> stuffToHighlight, EditingPreferences ep)
	{
		// make sets of all of the arcs and nodes explicitly selected for deletion
		Set<ArcInst> arcsToDelete = new HashSet<ArcInst>();
		Set<NodeInst> nodesToDelete = new HashSet<NodeInst>();
		if (cantEdit(cell, null, true, false, true) != 0) return;
		for(Geometric geom : list)
		{
			if (geom instanceof ArcInst)
			{
				ArcInst ai = (ArcInst)geom;
				arcsToDelete.add(ai);
			} else if (geom instanceof NodeInst)
			{
				NodeInst ni = (NodeInst)geom;
				if (cantEdit(cell, ni, true, false, true) != 0) continue;
				nodesToDelete.add(ni);
			}
		}

		// make a set of additional nodes to potentially delete
		Set<NodeInst> alsoDeleteTheseNodes = new HashSet<NodeInst>();

		// also (potentially) delete nodes on the end of deleted arcs
		for(ArcInst ai : arcsToDelete)
		{
			alsoDeleteTheseNodes.add(ai.getHeadPortInst().getNodeInst());
			alsoDeleteTheseNodes.add(ai.getTailPortInst().getNodeInst());
		}

		// also mark all nodes on the other end of arcs connected to erased nodes
		for(NodeInst ni : nodesToDelete)
		{
			for(Iterator<Connection> sit = ni.getConnections(); sit.hasNext(); )
			{
				Connection con = sit.next();
				ArcInst ai = con.getArc();
				int otherEnd = 1 - con.getEndIndex();
				NodeInst oNi = ai.getPortInst(otherEnd).getNodeInst();
				alsoDeleteTheseNodes.add(oNi);
			}
		}

		// reconnect hair to cells (if requested)
		if (reconstructArcsAndExports)
		{
			for(NodeInst ni : nodesToDelete)
			{
				if (!ni.isCellInstance()) continue;

				// make a map of recreated ports
				Map<PortInst,PortInst> reassigned = new HashMap<PortInst,PortInst>();

				// reconstruct exports to deleted cell instances
				for(Iterator<Export> eIt = ni.getExports(); eIt.hasNext(); )
				{
					Export e = eIt.next();
					FixpTransform trans = new FixpTransform();
					Orientation orient = Orientation.IDENT;
					PortInst pi = e.getOriginalPort();
					NodeInst subNi;
					PortProto subPP;
					for(;;)
					{
						subNi = pi.getNodeInst();
						subPP = pi.getPortProto();
						trans = subNi.rotateOut(trans);
						orient = orient.concatenate(subNi.getOrient());
						if (!subNi.isCellInstance()) break;
						trans = subNi.translateOut(trans);
						pi = ((Export)subPP).getOriginalPort();
					}
					NodeProto subNp = subNi.getProto();
					Point2D ctr = new Point2D.Double(subNi.getTrueCenter().getX(), subNi.getTrueCenter().getY());
					trans.transform(ctr, ctr);
					NodeInst eNi = NodeInst.makeInstance(subNp, ep, ctr, subNi.getXSize(), subNi.getYSize(),
						cell, orient, null);
					pi = eNi.findPortInstFromEquivalentProto(subPP);
					reassigned.put(e.getOriginalPort(), pi);
					e.move(pi);
					if (stuffToHighlight != null) stuffToHighlight.add(e);
				}

				// reconstruct each connection to a deleted cell instance
				for(Iterator<Connection> cIt = ni.getConnections(); cIt.hasNext(); )
				{
					Connection con = cIt.next();
					ArcInst ai = con.getArc();
					if (arcsToDelete.contains(ai)) continue;

					// recreate them
					int thisEnd = con.getEndIndex();
					int otherEnd = 1 - thisEnd;
					PortInst thisPi = ai.getPortInst(thisEnd);
					PortInst otherPi = ai.getPortInst(otherEnd);
					NodeInst otherNi = otherPi.getNodeInst();
					if (otherNi == ni)
					{
						// special case: arc from node to itself gets preserved?
						continue;
					}
					if (nodesToDelete.contains(otherNi)) continue;

					// reconnect a piece of hair to a cell instance
					PortInst alreadyPI = reassigned.get(thisPi);
					if (alreadyPI == null)
					{
						PrimitiveNode pinNp = ai.getProto().findPinProto();
						NodeInst pin = NodeInst.makeInstance(pinNp, ep, con.getLocation(), pinNp.getDefWidth(ep), pinNp.getDefHeight(ep), cell);
						alreadyPI = pin.getOnlyPortInst();
						reassigned.put(thisPi, alreadyPI);
					}
					ArcInst newAI = ArcInst.makeInstanceBase(ai.getProto(), ep, ai.getLambdaBaseWidth(), otherPi, alreadyPI,
						ai.getConnection(otherEnd).getLocation(), con.getLocation(), ai.getName());
					if (stuffToHighlight != null) stuffToHighlight.add(newAI);
					alsoDeleteTheseNodes.remove(otherNi);
				}
			}
		}

		// now kill all of the arcs
		for(ArcInst ai : arcsToDelete)
		{
			ai.kill();
		}

		// reconnect all nodes that are in the middle of two arcs
		for(NodeInst ni : nodesToDelete)
		{
//			if (reconstructArcsAndExports && ni.isCellInstance()) continue;

			// see if any arcs can be reconnected as a result of this kill
			Reconnect re = Reconnect.erasePassThru(ni, false, false, ep);
			if (re != null) re.reconnectArcs(ep);
		}

		// next kill all of the nodes
		cell.killNodes(nodesToDelete);

		// kill all pin nodes that touched an arc and no longer do
		Set<NodeInst> deleteTheseNodes = new HashSet<NodeInst>();
		for(NodeInst ni : alsoDeleteTheseNodes)
		{
			if (!ni.isLinked()) continue;
			if (!ni.isCellInstance())
			{
				if (!ni.getProto().getFunction().isPin()) continue;
				if (ni.hasConnections() || ni.hasExports()) continue;
				deleteTheseNodes.add(ni);
			}
		}
		cell.killNodes(deleteTheseNodes);

		// kill all unexported pin or bus nodes left in the middle of arcs
		List<NodeInst> nodesToPassThru = new ArrayList<NodeInst>();
		for(NodeInst ni : alsoDeleteTheseNodes)
		{
			if (!ni.isLinked()) continue;
			if (!ni.isCellInstance())
			{
				if (!ni.getProto().getFunction().isPin()) continue;
				if (ni.hasExports()) continue;
				if (!ni.isInlinePin()) continue;
				nodesToPassThru.add(ni);
			}
		}

		for(NodeInst ni : nodesToPassThru)
		{
			Reconnect re = Reconnect.erasePassThru(ni, false, false, ep);
			if (re != null)
			{
				re.reconnectArcs(ep);
				ni.kill();
			}
		}

		int numArcsDeleted = arcsToDelete.size();
		int numNodesDeleted = nodesToDelete.size();
		if (numArcsDeleted != 0 || numNodesDeleted != 0)
		{
			String msg = "Deleted";
			if (numNodesDeleted == 1) msg += " 1 node"; else
				if (numNodesDeleted > 1) msg += " " + numNodesDeleted + " nodes";
			if (numNodesDeleted > 0 && numArcsDeleted > 0) msg += " and";
			if (numArcsDeleted == 1) msg += " 1 arc"; else
				if (numArcsDeleted > 1) msg += " " + numArcsDeleted + " arcs";
			msg += " in cell " + cell.describe(false);
			System.out.println(msg);
		}
	}

	/****************************** CLEAN-UP ******************************/

	/**
	 * This class implements the changes needed to cleanup pins in a Cell.
	 */
	public static class CleanupChanges extends Job
	{
		private Cell cell;
		private boolean justThis;
		private Set<NodeInst> pinsToRemove;
		private List<Reconnect> pinsToPassThrough;
		private Map<NodeInst,EPoint> pinsToScale;
		private List<NodeInst> textToMove;
		private Set<ArcInst> arcsToKill;
		private int zeroSize, negSize, overSizePins;

		public CleanupChanges(Cell cell, boolean justThis, Set<NodeInst> pinsToRemove, List<Reconnect> pinsToPassThrough,
			Map<NodeInst,EPoint> pinsToScale, List<NodeInst> textToMove, Set<ArcInst> arcsToKill,
			int zeroSize, int negSize, int overSizePins)
		{
			super("Cleanup " + cell, User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.justThis = justThis;
			this.pinsToRemove = pinsToRemove;
			this.pinsToPassThrough = pinsToPassThrough;
			this.pinsToScale = pinsToScale;
			this.textToMove = textToMove;
			this.arcsToKill = arcsToKill;
			this.zeroSize = zeroSize;
			this.negSize = negSize;
			this.overSizePins = overSizePins;
		}

        @Override
		public boolean doIt() throws JobException
		{
            EditingPreferences ep = getEditingPreferences();
			// make sure moving the node is allowed
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			// do the queued operations
			cell.killNodes(pinsToRemove);
			int pinsPassedThrough = 0;
			for(;;)
			{
				boolean found = false;
				for(Reconnect re : pinsToPassThrough)
				{
					if (!re.ni.isLinked()) continue;
					List<ArcInst> created = re.reconnectArcs(ep);
					if (created.size() > 0)
					{
						re.ni.kill();
						pinsPassedThrough++;
						found = true;
					}
				}
				if (!found) break;
				pinsToPassThrough = getPinsToPassThrough(cell, ep);
			}
			for(NodeInst ni : pinsToScale.keySet())
			{
				EPoint scale = pinsToScale.get(ni);
				ni.resize(scale.getX(), scale.getY());
			}
			for(NodeInst ni : textToMove)
			{
				ni.invisiblePinWithOffsetText(true);
			}
            cell.killArcs(arcsToKill);

			// report what was cleaned
			StringBuffer infstr = new StringBuffer();
			if (!justThis) infstr.append("Cell " + cell.describe(true) + ":");
			boolean spoke = false;
			if ((pinsToRemove.size()+pinsPassedThrough) != 0)
			{
				int removed = pinsToRemove.size() + pinsPassedThrough;
				infstr.append("Removed " + removed + " pins");
				spoke = true;
			}
			if (arcsToKill.size() != 0)
			{
				if (spoke) infstr.append("; ");
				infstr.append("Removed " + arcsToKill.size() + " duplicate arcs");
				spoke = true;
			}
			if (pinsToScale.size() != 0)
			{
				if (spoke) infstr.append("; ");
				infstr.append("Shrunk " + pinsToScale.size() + " pins");
				spoke = true;
			}
			if (zeroSize != 0)
			{
				if (spoke) infstr.append("; ");
				if (justThis)
				{
					infstr.append("Highlighted " + zeroSize + " zero-size pins");
				} else
				{
					infstr.append("Found " + zeroSize + " zero-size pins");
				}
				spoke = true;
			}
			if (negSize != 0)
			{
				if (spoke) infstr.append("; ");
				if (justThis)
				{
					infstr.append("Highlighted " + negSize + " negative-size pins");
				} else
				{
					infstr.append("Found " + negSize + " negative-size pins");
				}
				spoke = true;
			}
			if (overSizePins != 0)
			{
				if (spoke) infstr.append("; ");
				if (justThis)
				{
					infstr.append("Highlighted " + overSizePins + " oversize pins with arcs that don't touch");
				} else
				{
					infstr.append("Found " + overSizePins + " oversize pins with arcs that don't touch");
				}
				spoke = true;
			}
			if (textToMove.size() != 0)
			{
				if (spoke) infstr.append("; ");
				infstr.append("Moved text on " + textToMove.size() + " pins with offset text");
			}
			System.out.println(infstr.toString());
			return true;
		}
	}

	/**
	 * This class implements the changes needed to shorten selected arcs.
	 */
	public static class ShortenArcs extends Job
	{
		private Cell cell;
		private List<ArcInst> selected;

		public ShortenArcs(Cell cell, List<ArcInst> selected)
		{
			super("Shorten selected arcs", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.selected = selected;
			startJob();
		}

		public boolean doIt() throws JobException
		{
			// make sure shortening is allowed
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			double [] dX = new double[2];
			double [] dY = new double[2];
			List<String> arcsShortened = new ArrayList<String>();
			for(ArcInst ai : selected)
			{
				for(int j=0; j<2; j++)
				{
					Poly portPoly = ai.getPortInst(j).getPoly();
					double wid = ai.getLambdaBaseWidth();
					portPoly.reducePortPoly(ai.getPortInst(j), wid, ai.getAngle());
					Point2D closest = portPoly.closestPoint(ai.getLocation(1-j));
					dX[j] = closest.getX() - ai.getLocation(j).getX();
					dY[j] = closest.getY() - ai.getLocation(j).getY();
				}
				if (dX[0] != 0 || dY[0] != 0 || dX[1] != 0 || dY[1] != 0)
				{
					ai.modify(dX[ArcInst.HEADEND], dY[ArcInst.HEADEND], dX[ArcInst.TAILEND], dY[ArcInst.TAILEND]);
					arcsShortened.add(ai.describe(false));
				}
			}
			int count = arcsShortened.size();
			String msg = "Shortened " + count + " arcs";
			if (count > 0) msg += ":";
			System.out.println(msg);
			Collections.sort(arcsShortened);
			for(String arcName : arcsShortened) System.out.println("   " + arcName);
			return true;
		}
	}

	/****************************** MOVE SELECTED OBJECTS ******************************/

	public static class ManyMove extends Job
	{
		private static final boolean verbose = true;
		private Cell cell;
		private List<ElectricObject> highlightedObjs;
		private List<DisplayedText> highlightedText;
		private double dX, dY;
		private boolean updateStatusBar;
		private boolean moveNodeWithExport; // User's preference

		public ManyMove(Cell cell, List<ElectricObject> highlightedObjs, List<DisplayedText> highlightedText, double dX, double dY)
		{
			super("Move", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.highlightedObjs = highlightedObjs;
			this.highlightedText = highlightedText;
			this.dX = dX;
			this.dY = dY;
			this.moveNodeWithExport = User.isMoveNodeWithExport();
			startJob();
		}

		public boolean doIt() throws JobException
		{
			// get information about what is highlighted
			if (highlightedObjs.size() + highlightedText.size() == 0) return false;

			// make sure moving is allowed
			if (cantEdit(cell, null, true, false, true) != 0) return false;

			// special case if moving only one node
			if (highlightedObjs.size() == 1 && highlightedText.size() == 0)
			{
				ElectricObject firstEObj = highlightedObjs.get(0);
				if (firstEObj instanceof NodeInst || firstEObj instanceof PortInst)
				{
					NodeInst ni;
					if (firstEObj instanceof PortInst) {
						ni = ((PortInst)firstEObj).getNodeInst();
					} else {
						ni = (NodeInst)firstEObj;
					}

					// make sure moving the node is allowed
					if (cantEdit(cell, ni, true, false, true) != 0) return false;

					ni.move(dX, dY);
					if (verbose) System.out.println("Moved node " + ni.describe(false) + " by (" + TextUtils.formatDistance(dX) + "," + TextUtils.formatDistance(dY) + ")");
					updateStatusBar = true;
					fieldVariableChanged("updateStatusBar");
					return true;
				}
			}

			// special case if moving diagonal fixed-angle arcs connected to single Manhattan arcs
			boolean found = false;
			for(ElectricObject eobj : highlightedObjs)
			{
				if (eobj instanceof ArcInst)
				{
					ArcInst ai = (ArcInst)eobj;
					if (ai.getHeadLocation().getX() != ai.getTailLocation().getX() &&
						ai.getHeadLocation().getY() != ai.getTailLocation().getY())
					{
						if (ai.isFixedAngle() && !ai.isRigid())
						{
							int j;
							for(j=0; j<2; j++)
							{
								NodeInst ni = ai.getPortInst(j).getNodeInst();
								ArcInst oai = null;
								for(Iterator<Connection> pIt = ni.getConnections(); pIt.hasNext(); )
								{
									Connection con = pIt.next();
									if (con.getArc() == ai) continue;
									if (oai == null) oai = con.getArc(); else
									{
										oai = null;
										break;
									}
								}
								if (oai == null) break;
								if (oai.getHeadLocation().getX() != oai.getTailLocation().getX() &&
									oai.getHeadLocation().getY() != oai.getTailLocation().getY()) break;
								if (highlightedObjs.contains(oai)) break;
							}
							if (j >= 2) { found = true;   break; }
						}
					}
				}
			}
			if (found)
			{
				// meets the test: make the special move to slide other orthogonal arcs
				for(ElectricObject eobj : highlightedObjs)
				{
					if (!(eobj instanceof ArcInst)) continue;
					ArcInst ai = (ArcInst)eobj;

					double [] deltaXs = new double[2];
					double [] deltaYs = new double[2];
					NodeInst [] niList = new NodeInst[2];
					deltaXs[0] = deltaYs[0] = deltaXs[1] = deltaYs[1] = 0;
					int arcangle = ai.getDefinedAngle();
					int j;
					for(j=0; j<2; j++)
					{
						NodeInst ni = ai.getPortInst(j).getNodeInst();
						niList[j] = ni;
						ArcInst oai = null;
						for(Iterator<Connection> pIt = ni.getConnections(); pIt.hasNext(); )
						{
							Connection con = pIt.next();
							if (con.getArc() != ai) { oai = con.getArc();   break; }
						}
						if (oai == null) break;
						if (DBMath.doublesEqual(oai.getHeadLocation().getX(), oai.getTailLocation().getX()))
						{
							Point2D iPt = DBMath.intersect(oai.getHeadLocation(), 900,
								new Point2D.Double(ai.getHeadLocation().getX()+dX, ai.getHeadLocation().getY()+dY), arcangle);
							if (iPt != null)
							{
								deltaXs[j] = iPt.getX() - ai.getLocation(j).getX();
								deltaYs[j] = iPt.getY() - ai.getLocation(j).getY();
							}
						} else if (DBMath.doublesEqual(oai.getHeadLocation().getY(), oai.getTailLocation().getY()))
						{
							Point2D iPt = DBMath.intersect(oai.getHeadLocation(), 0,
								new Point2D.Double(ai.getHeadLocation().getX()+dX, ai.getHeadLocation().getY()+dY), arcangle);
							if (iPt != null)
							{
								deltaXs[j] = iPt.getX() - ai.getLocation(j).getX();
								deltaYs[j] = iPt.getY() - ai.getLocation(j).getY();
							}
						}
					}
					if (j < 2) continue;
					NodeInst.modifyInstances(niList, deltaXs, deltaYs, null, null);
				}
				if (verbose) System.out.println("Moved multiple objects by (" + TextUtils.formatDistance(dX) +
					"," + TextUtils.formatDistance(dY) + ")");
				updateStatusBar = true;
				fieldVariableChanged("updateStatusBar");
				return true;
			}

			// special case if moving only arcs and they slide
			boolean onlySlidable = true, foundArc = false;
			for(ElectricObject eobj : highlightedObjs)
			{
				if (eobj instanceof ArcInst)
				{
					ArcInst ai = (ArcInst)eobj;
					foundArc = true;
					// see if the arc moves in its ports
					if (ai.isSlidable())
					{
						Point2D newHead = new Point2D.Double(ai.getHeadLocation().getX()+dX, ai.getHeadLocation().getY()+dY);
						Point2D newTail = new Point2D.Double(ai.getTailLocation().getX()+dX, ai.getTailLocation().getY()+dY);
						if (ai.headStillInPort(newHead, true) && ai.tailStillInPort(newTail, true)) continue;
					}
				}
				onlySlidable = false;
			}
			if (foundArc && onlySlidable)
			{
				for(ElectricObject eobj : highlightedObjs)
				{
					if (eobj instanceof ArcInst)
					{
						ArcInst ai = (ArcInst)eobj;
						ai.modify(dX, dY, dX, dY);
						if (verbose) System.out.println("Moved arc " + ai.describe(false) + " by (" + TextUtils.formatDistance(dX) + "," + TextUtils.formatDistance(dY) + ")");
					}
				}
				updateStatusBar = true;
				fieldVariableChanged("updateStatusBar");
				return true;
			}

			// ignore arc motion if the nodes are locked
			for(int i=0; i<highlightedObjs.size(); i++)
			{
				ElectricObject eobj = highlightedObjs.get(i);
				if (eobj instanceof ArcInst)
				{
					ArcInst ai = (ArcInst)eobj;
					int errorCode = cantEdit(cell, ai.getHeadPortInst().getNodeInst(), true, false, true);
					if (errorCode < 0) return false;
					if (errorCode > 0)
					{
						highlightedObjs.remove(i);
						i--;
						continue;
					}

					errorCode = cantEdit(cell, ai.getTailPortInst().getNodeInst(), true, false, true);
					if (errorCode < 0) return false;
					if (errorCode > 0)
					{
						highlightedObjs.remove(i);
						i--;
						continue;
					}
				}
			}

			// make flag to track the nodes that move
			Set<NodeInst> flag = new HashSet<NodeInst>();

			// remember the location of every node and arc
			Map<NodeInst,Point2D.Double> nodeLocation = new HashMap<NodeInst,Point2D.Double>();
			for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
			{
				NodeInst ni = it.next();
				nodeLocation.put(ni, new Point2D.Double(ni.getAnchorCenterX(), ni.getAnchorCenterY()));
			}
			Map<ArcInst,Point2D.Double> arcLocation = new HashMap<ArcInst,Point2D.Double>();
			for(Iterator<ArcInst> it = cell.getArcs(); it.hasNext(); )
			{
				ArcInst ai = it.next();
				arcLocation.put(ai, new Point2D.Double(ai.getTrueCenterX(), ai.getTrueCenterY()));
			}

			// mark all nodes that want to move
			for(ElectricObject eobj : highlightedObjs)
			{
				if (eobj instanceof PortInst) eobj = ((PortInst)eobj).getNodeInst();
				if (eobj instanceof NodeInst)
				{
					NodeInst ni = (NodeInst)eobj;
					if (Generic.isCellCenter(ni)) // in this case, there are more elements to move
						continue; 
					flag.add(ni);
				} else if (eobj instanceof ArcInst)
				{
					ArcInst ai = (ArcInst)eobj;

					NodeInst ni1 = ai.getHeadPortInst().getNodeInst();
                    Point2D newHead = new Point2D.Double(ai.getHeadLocation().getX()+dX, ai.getHeadLocation().getY()+dY);
					if (!(ai.isSlidable() && ai.headStillInPort(newHead, true))) {
                        flag.add(ni1);
                    }

					NodeInst ni2 = ai.getTailPortInst().getNodeInst();
					Point2D newTail = new Point2D.Double(ai.getTailLocation().getX()+dX, ai.getTailLocation().getY()+dY);
                    if (!(ai.isSlidable() && ai.tailStillInPort(newTail, true))) {
    					flag.add(ni2);
                    }

					Layout.setTempRigid(ai, true);
				}
			}

			// count the number of nodes that will move
			int numNodes = 0;
			for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
			{
				NodeInst ni = it.next();
				if (!flag.contains(ni)) continue;

				// make sure moving the node is allowed
				int errorCode = cantEdit(cell, ni, true, false, true);
				if (errorCode < 0) return false;
				if (errorCode > 0)
				{
					flag.remove(ni);
					continue;
				}
				numNodes++;
			}

			// look at all nodes and move them appropriately
			if (numNodes > 0)
			{
				NodeInst [] nis = new NodeInst[numNodes];
				double [] dXs = new double[numNodes];
				double [] dYs = new double[numNodes];
				numNodes = 0;
				for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
				{
					NodeInst ni = it.next();
					if (!flag.contains(ni)) continue;
					nis[numNodes] = ni;
					dXs[numNodes] = dX;
					dYs[numNodes] = dY;
					numNodes++;
				}
				NodeInst.modifyInstances(nis, dXs, dYs, null, null);
			}
			flag = null;

			// look at all arcs and move them appropriately
			for(ElectricObject eobj : highlightedObjs)
			{
				if (!(eobj instanceof ArcInst)) continue;
				ArcInst ai = (ArcInst)eobj;
				Point2D pt = arcLocation.get(ai);
				if (pt.getX() != ai.getTrueCenterX() ||
					pt.getY() != ai.getTrueCenterY()) continue;

				// see if the arc moves in its ports
				boolean headInPort = false, tailInPort = false;
				if (!ai.isRigid() && ai.isSlidable())
				{
					headInPort = ai.headStillInPort(
						new Point2D.Double(ai.getHeadLocation().getX()+dX, ai.getHeadLocation().getY()+dY), true);
					tailInPort = ai.tailStillInPort(
						new Point2D.Double(ai.getTailLocation().getX()+dX, ai.getTailLocation().getY()+dY), true);
				}

				// if both ends slide in their port, move the arc
				if (headInPort && tailInPort)
				{
					ai.modify(dX, dY, dX, dY);
					continue;
				}

				// if neither end can slide in its port, move the nodes
				if (!headInPort && !tailInPort)
				{
					for(int k=0; k<2; k++)
					{
						NodeInst ni;
						if (k == 0) ni = ai.getHeadPortInst().getNodeInst(); else
							ni = ai.getTailPortInst().getNodeInst();
						Point2D nPt = nodeLocation.get(ni);
						if (ni.getAnchorCenterX() != nPt.getX() || ni.getAnchorCenterY() != nPt.getY()) continue;

						// fix all arcs that aren't sliding
						for(ElectricObject oEObj : highlightedObjs)
						{
							if (oEObj instanceof ArcInst)
							{
								ArcInst oai = (ArcInst)oEObj;
								Point2D aPt = arcLocation.get(oai);
								if (aPt.getX() != oai.getTrueCenterX() ||
									aPt.getY() != oai.getTrueCenterY()) continue;
								if (oai.headStillInPort(
										new Point2D.Double(ai.getHeadLocation().getX()+dX, ai.getHeadLocation().getY()+dY), true) ||
									oai.tailStillInPort(
										new Point2D.Double(ai.getTailLocation().getX()+dX, ai.getTailLocation().getY()+dY), true))
											continue;
								Layout.setTempRigid(oai, true);
							}
						}
						ni.move(dX - (ni.getAnchorCenterX() - nPt.getX()),
							dY - (ni.getAnchorCenterY() - nPt.getY()));
					}
					continue;
				}
			}

			// also move selected text
			moveSelectedText(cell, highlightedText, moveNodeWithExport);
			if (verbose) 
			{
				String msg = (moveNodeWithExport) ? "Moved multiple objects (" : "Moved text (";
				System.out.println(msg + TextUtils.formatDistance(dX) + "," + TextUtils.formatDistance(dY) + ")");
			}
			updateStatusBar = true;
			fieldVariableChanged("updateStatusBar");
			return true;
		}

		public void terminateOK()
		{
			if (updateStatusBar) StatusBar.updateStatusBar();
		}

		/**
		 * Method to move the "numtexts" text objects described (as highlight strings)
		 * in the array "textlist", by "odx" and "ody".  Geometry objects in "list" (NOGEOM-terminated)
		 * and the "total" nodes in "nodelist" have already been moved, so don't move any text that
		 * is on these objects.
		 */
		private void moveSelectedText(Cell cell, List<DisplayedText> highlightedText, boolean moveNodeWithExport)
		{
			for(DisplayedText dt : highlightedText)
			{
				// disallow moving if lock is on
				if (cell != null)
				{
					int errorCode = cantEdit(cell, null, true, true, true);
					if (errorCode < 0) return;
					if (errorCode > 0) continue;
				}

				// handle nodes that move with text
				ElectricObject eobj = dt.getElectricObject();
				if (dt.movesWithText(moveNodeWithExport))
				{
					NodeInst ni = null;
					if (eobj instanceof NodeInst) ni = (NodeInst)eobj;
					if (eobj instanceof Export) ni = ((Export)eobj).getOriginalPort().getNodeInst();
					if (ni != null)
					{
						ni.move(dX, dY);
						continue;
					}
				}

				// moving variable on object
				Variable.Key varKey = dt.getVariableKey();
				TextDescriptor td = eobj.getTextDescriptor(varKey);
				if (td == null) continue;
				NodeInst ni = null;
				if (eobj instanceof NodeInst) ni = (NodeInst)eobj; else
					if (eobj instanceof PortInst) ni = ((PortInst)eobj).getNodeInst(); else
						if (eobj instanceof Export && varKey == Export.EXPORT_NAME) ni = ((Export)eobj).getOriginalPort().getNodeInst();
                double dX = this.dX > 0 ? Math.max(this.dX, 0.25) : this.dX < 0 ? Math.min(this.dX, -0.25) : 0;
                double dY = this.dY > 0 ? Math.max(this.dY, 0.25) : this.dY < 0 ? Math.min(this.dY, -0.25) : 0;
				if (ni != null)
				{
					Point2D curLoc = new Point2D.Double(ni.getAnchorCenterX()+td.getXOff(), ni.getAnchorCenterY()+td.getYOff());
					FixpTransform rotateOut = ni.rotateOut();
					rotateOut.transform(curLoc, curLoc);
					curLoc.setLocation(curLoc.getX()+dX, curLoc.getY()+dY);
					FixpTransform rotateIn = ni.rotateIn();
					rotateIn.transform(curLoc, curLoc);
					eobj.setOff(varKey, curLoc.getX()-ni.getAnchorCenterX(), curLoc.getY()-ni.getAnchorCenterY());
				} else
				{
					eobj.setOff(varKey, td.getXOff()+dX, td.getYOff()+dY);
				}
			}
		}
	}

	/****************************** RECENTER CELL ON SELECTION ******************************/

	public static class CellCenterToCenterOfSelection extends Job
	{
		private Cell cell;
		private EPoint ctr;

		public CellCenterToCenterOfSelection(Cell cell, EPoint ctr)
		{
			super("Cell center to center of selection", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.ctr = ctr;
			startJob();
		}

		public boolean doIt() throws JobException
		{
            NodeInst center = null;
            for(NodeInst ni : i2i(cell.getNodes()))
                if (Generic.isCellCenter(ni))
                    center = ni;
            if (center == null)
            {
                System.out.println("No cell center; please add one.");
                return false;
            }

//            System.out.println("Moving cell center to (" + ctr.getX() + "," + ctr.getY() + ")");
            center.move(ctr.getX(), ctr.getY());
            return true;
		}
	}


	/**
	 * Store information of new arc to be created that reconnects
	 * two arcs that will be deleted
	 */
	private static class ReconnectedArc implements Serializable
	{
		/** port at other end of arc */						private PortInst [] reconPi;
		/** coordinate at other end of arc */				private EPoint [] recon;
		/** old arc instances that will be deleted */       private ArcInst [] reconAr;
		/** prototype of new arc */							private ArcProto ap;
		/** width of new arc */								private double wid;
		/** true to make new arc have arrow on head */		private boolean directionalHead;
		/** true to make new arc have arrow on tail */		private boolean directionalTail;
		/** true to make new arc have arrow on body */		private boolean directionalBody;
		/** true to extend the head of the new arc */		private boolean extendHead;
		/** true to extend the tail of the new arc */		private boolean extendTail;
		/** true to negate the head of the new arc */		private boolean negateHead;
		/** true to negate the tail of the new arc */		private boolean negateTail;
		/** the name to use on the reconnected arc */		private String arcName;
		/** TextDescriptor for the reconnected arc name */	private TextDescriptor arcNameTD;
	}

	public static List<Reconnect> getPinsToPassThrough(Cell cell, EditingPreferences ep)
	{
		List<Reconnect> pinsToPassThrough = new ArrayList<Reconnect>();
		for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
		{
			NodeInst ni = it.next();
			if (!ni.getFunction().isPin()) continue;

			// if the pin is an export, save it
			if (ni.hasExports()) continue;

			// if the pin is connected to two arcs along the same slope, delete it
			if (ni.isInlinePin())
			{
				Reconnect re = Reconnect.erasePassThru(ni, false, true, ep);
				if (re != null)
				{
					pinsToPassThrough.add(re);
				}
			}
		}
		return pinsToPassThrough;
	}

	/**
	 * This class handles deleting pins that are between two arcs,
	 * and reconnecting the arcs without the pin.
	 */
	public static class Reconnect implements Serializable
	{
		/** node in the center of the reconnect */			private NodeInst ni;
		/** list of reconnected arcs */                     private ArrayList<ReconnectedArc> reconnectedArcs;

        public Reconnect() {
        }

		/**
		 * Method to find a possible Reconnect through a given NodeInst.
		 * @param ni the NodeInst to examine.
		 * @param allowdiffs true to allow differences in the two arcs.
		 * If this is false, then different width arcs, or arcs that are not lined up
		 * precisely, will not be considered for reconnection.
		 * @param checkPermission true to check that the node can be changed.
         * @param ep EditingPreferences
		 * @return a Reconnect object that describes the reconnection to be done.
		 * Returns null if no reconnection can be found.
		 */
		public static Reconnect erasePassThru(NodeInst ni, boolean allowdiffs, boolean checkPermission, EditingPreferences ep)
		{
			// stop now if nothing connected to the node
			if (!ni.hasConnections()) return null;

			// disallow erasing if lock is on
			Cell cell = ni.getParent();
			if (checkPermission && cantEdit(cell, ni, true, false, true) != 0) return null;

			Reconnect recon = new Reconnect();
			recon.ni = ni;
			recon.reconnectedArcs = new ArrayList<ReconnectedArc>();

			// get all arcs connected to each portinst on node
			for (Iterator<PortInst> it = ni.getPortInsts(); it.hasNext(); ) {
				PortInst pi = it.next();

				List<ArcInst> arcs = new ArrayList<ArcInst>();
				for (Iterator<Connection> it2 = pi.getConnections(); it2.hasNext(); ) {
					Connection conn = it2.next();
					ArcInst ai = conn.getArc();
					// ignore arcs that connect from the node to itself
					if (ai.getHeadPortInst().getNodeInst() == ai.getTailPortInst().getNodeInst())
						continue;
					arcs.add(ai);
				}

				// go through all arcs on this portinst and see if any can be reconnected
				while (arcs.size() > 1) {
					ArcInst ai1 = arcs.remove(0);
					for (ArcInst ai2 : arcs) {
						ReconnectedArc ra = reconnectArcs(pi, ai1, ai2, allowdiffs);
						// if reconnection to be made, add to list
						if (ra != null) recon.reconnectedArcs.add(ra);
					}
				}
			}
			if (recon.reconnectedArcs.isEmpty()) return null;

			return recon;
		}

		/** Returns null if couldn't reconnect arcs together */
		private static ReconnectedArc reconnectArcs(PortInst pi, ArcInst ai1, ArcInst ai2, boolean allowdiffs) {

			// verify that the two arcs to merge have the same type
			if (ai1.getProto() != ai2.getProto()) return null;

			ReconnectedArc ra = new ReconnectedArc();
			ra.ap = ai1.getProto();
			ra.reconPi = new PortInst[2];
			ra.recon = new EPoint[2];
			ra.reconAr = new ArcInst[2];
			ra.reconAr[0] = ai1;
			ra.reconAr[1] = ai2;
			Point2D [] orig = new Point2D[2];               // end points on port that will be deleted
			Point2D [] delta = new Point2D[2];              // deltaX,Y of arc

			// get end points of arcs
			for (int i=0; i<2; i++) {
				if (ai1.getPortInst(i) != pi) {
					ra.reconPi[0] = ai1.getPortInst(i);
					ra.recon[0] = ai1.getLocation(i);
				} else {
					orig[0] = ai1.getLocation(i);
				}
				if (ai2.getPortInst(i) != pi) {
					ra.reconPi[1] = ai2.getPortInst(i);
					ra.recon[1] = ai2.getLocation(i);
				} else {
					orig[1] = ai2.getLocation(i);
				}
			}
			delta[0] = new Point2D.Double(ra.recon[0].getX() - orig[0].getX(),
										  ra.recon[0].getY() - orig[0].getY());
			delta[1] = new Point2D.Double(ra.recon[1].getX() - orig[1].getX(),
										  ra.recon[1].getY() - orig[1].getY());
			if (!allowdiffs)
			{
				// verify that the two arcs to merge have the same width
				if (ai1.getLambdaBaseWidth() != ai2.getLambdaBaseWidth()) return null;

				// verify that the two arcs have the same slope
				if ((delta[1].getX()*delta[0].getY()) != (delta[0].getX()*delta[1].getY())) return null;

				// verify that the angle between two arcs is obtuse
				boolean zeroLength = false;
				if (delta[0].getX() == 0 && delta[0].getY() == 0) zeroLength = true;
				if (delta[1].getX() == 0 && delta[1].getY() == 0) zeroLength = true;
				if (!zeroLength && delta[0].getX()*delta[1].getX() + delta[0].getY()*delta[1].getY() >= 0) return null;

				if (orig[0].getX() != orig[1].getX() || orig[0].getY() != orig[1].getY())
				{
					// did not connect at the same location: be sure that angle is consistent
					if (delta[0].getX() != 0 || delta[0].getY() != 0)
					{
						if (((orig[0].getX()-orig[1].getX())*delta[0].getY()) !=
							(delta[0].getX()*(orig[0].getY()-orig[1].getY()))) return null;
					} else if (delta[1].getX() != 0 || delta[1].getY() != 0)
					{
						if (((orig[0].getX()-orig[1].getX())*delta[1].getY()) !=
							(delta[1].getX()*(orig[0].getY()-orig[1].getY()))) return null;
					} else return null;
				}
			}

			// OK to connect arcs
			ra.wid = ai1.getLambdaBaseWidth();

			ra.directionalHead = ai1.isHeadArrowed();
			ra.directionalTail = ai1.isTailArrowed();
			ra.directionalBody = ai1.isBodyArrowed();
			ra.extendHead = ai1.isHeadExtended();
			ra.extendTail = ai2.isTailExtended();
			ra.negateHead = ai1.isHeadNegated();
			ra.negateTail = ai2.isTailNegated();
			if (ai1.getName() != null && !ai1.getNameKey().isTempname())
			{
				ra.arcName = ai1.getName();
				ra.arcNameTD = ai1.getTextDescriptor(ArcInst.ARC_NAME);
			}
			if (ai2.getName() != null && !ai2.getNameKey().isTempname())
			{
				ra.arcName = ai2.getName();
				ra.arcNameTD = ai2.getTextDescriptor(ArcInst.ARC_NAME);
			}

			return ra;
		}

		/**
		 * Method to implement the reconnection in this Reconnect.
		 * @return list of newly created ArcInst that reconnects.
		 */
		public List<ArcInst> reconnectArcs(EditingPreferences ep)
		{
			List<ArcInst> newArcs = new ArrayList<ArcInst>();

			// reconnect the arcs
			for (ReconnectedArc ra : reconnectedArcs)
			{
				if (!ra.reconPi[0].getNodeInst().isLinked() || !ra.reconPi[1].getNodeInst().isLinked()) continue;
				ArcInst newAi = ArcInst.makeInstanceBase(ra.ap, ep, ra.wid, ra.reconPi[0], ra.reconPi[1], ra.recon[0], ra.recon[1], null);
				if (newAi == null) continue;

				newAi.setHeadArrowed(ra.directionalHead);
				newAi.setTailArrowed(ra.directionalTail);
				newAi.setBodyArrowed(ra.directionalBody);
				newAi.setHeadExtended(ra.extendHead);
				newAi.setTailExtended(ra.extendTail);
				newAi.setHeadNegated(ra.negateHead);
				newAi.setTailNegated(ra.negateTail);
				if (ra.arcName != null)
				{
					newAi.setName(ra.arcName, ep);
					newAi.setTextDescriptor(ArcInst.ARC_NAME, ra.arcNameTD);
				}
				newAi.copyVarsFrom(ra.reconAr[0]);
				newAi.copyVarsFrom(ra.reconAr[1]);
				newArcs.add(newAi);
			}
			return newArcs;
		}
	}

	/**
	 * Method to spread circuitry.
	 * @param cell the cell in which spreading happens.
	 * @param ni the NodeInst about which spreading happens (may be null).
	 * @param direction the direction to spread: 'u' for up, 'd' for down, 'l' for left, 'r' for right.
	 * @param amount the distance to spread (negative values compact).
	 * @param lX the low X bound of the node (the edge of spreading).
	 * @param hX the high X bound of the node (the edge of spreading).
	 * @param lY the low Y bound of the node (the edge of spreading).
	 * @param hY the high Y bound of the node (the edge of spreading).
	 */
	public static void spreadCircuitry(Cell cell, NodeInst ni, char direction, double amount, double lX, double hX, double lY, double hY)
	{
		// disallow spreading if lock is on
		if (cantEdit(cell, null, true, false, true) != 0) return;

		// initialize a collection of Geometrics that have been seen
		Set<Geometric> geomSeen = new HashSet<Geometric>();

		// set "already done" flag for nodes Manhattan connected on spread line
		boolean mustBeHor = true;
		if (direction == 'l' || direction == 'r') mustBeHor = false;
		if (ni != null) manhattanTravel(ni, mustBeHor, geomSeen);

		// set "already done" flag for nodes that completely cover spread node or are in its line
		for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
		{
			NodeInst oNi = it.next();
			Rectangle2D r = oNi.getBaseShape().getBounds2D();
			if (direction == 'l' || direction == 'r')
			{
				if (r.getMinX() < lX && r.getMaxX() > hX)
					geomSeen.add(oNi);
				if (oNi.getTrueCenterX() == (lX+hX)/2)
					geomSeen.add(oNi);
			} else
			{
				if (r.getMinY() < lY && r.getMaxY() > hY)
					geomSeen.add(oNi);
				if (oNi.getTrueCenterY() == (lY+hY)/2)
					geomSeen.add(oNi);
			}
		}

		// mark those arcinsts that should stretch during spread
		for(Iterator<ArcInst> it = cell.getArcs(); it.hasNext(); )
		{
			ArcInst ai = it.next();
			NodeInst no1 = ai.getTailPortInst().getNodeInst();
			NodeInst no2 = ai.getHeadPortInst().getNodeInst();
			double xC1 = no1.getTrueCenterX();
			double yC1 = no1.getTrueCenterY();
			double xC2 = no2.getTrueCenterX();
			double yC2 = no2.getTrueCenterY();

			// if one node is along spread line, make it "no1"
			if (geomSeen.contains(no2))
			{
				NodeInst swapNi = no1;  no1 = no2;  no2 = swapNi;
				double swap = xC1;     xC1 = xC2;  xC2 = swap;
				swap = yC1;     yC1 = yC2;  yC2 = swap;
			}

			// if both nodes are along spread line, leave arc alone
			if (geomSeen.contains(no2)) continue;

			boolean i = true;
			if (geomSeen.contains(no1))
			{
				// handle arcs connected to spread line
				switch (direction)
				{
					case 'l': if (xC2 <= lX) i = false;   break;
					case 'r': if (xC2 >= hX) i = false;   break;
					case 'u': if (yC2 >= hY) i = false;   break;
					case 'd': if (yC2 <= lY) i = false;   break;
				}
			} else
			{
				// handle arcs that cross the spread line
				switch (direction)
				{
					case 'l': if (xC1 > lX && xC2 <= lX) i = false; else
						if (xC2 > lX && xC1 <= lX) i = false;
						break;
					case 'r': if (xC1 < hX && xC2 >= hX) i = false; else
						if (xC2 < hX && xC1 >= hX) i = false;
						break;
					case 'u': if (yC1 > hY && yC2 <= hY) i = false; else
						if (yC2 > hY && yC1 <= hY) i = false;
						break;
					case 'd': if (yC1 < lY && yC2 >= lY) i = false; else
						if (yC2 < lY && yC1 >= lY) i = false;
						break;
				}
			}
			if (!i) geomSeen.add(ai);
		}

		// now look at every nodeinst in the cell
		boolean moved = false;
		boolean again = true;
		while (again)
		{
			again = false;
			for(Iterator<NodeInst> it = cell.getNodes(); it.hasNext(); )
			{
				NodeInst oNi = it.next();

				// ignore this nodeinst if it has been spread already
				if (geomSeen.contains(oNi)) continue;

				// make sure nodeinst is on proper side of requested spread
				double xC1 = oNi.getTrueCenterX();
				double yC1 = oNi.getTrueCenterY();
				boolean doIt = false;
				switch (direction)
				{
					case 'l': if (xC1 < lX) doIt = true;   break;
					case 'r': if (xC1 > hX) doIt = true;   break;
					case 'u': if (yC1 > hY) doIt = true;   break;
					case 'd': if (yC1 < lY) doIt = true;   break;
				}
				if (!doIt) continue;

				// set every connecting nodeinst to be "spread"
				for(Iterator<ArcInst> aIt = cell.getArcs(); aIt.hasNext(); )
				{
					ArcInst ai = aIt.next();
					if (geomSeen.contains(ai))
					{
						// make arc temporarily nonrigid
						Layout.setTempRigid(ai, false);
					} else
					{
						// make arc temporarily rigid
						Layout.setTempRigid(ai, true);
					}
				}
				netTravel(oNi, geomSeen);

				// move this nodeinst in proper direction to do spread
				switch(direction)
				{
					case 'l':
						oNi.move(-amount, 0);
						break;
					case 'r':
						oNi.move(amount, 0);
						break;
					case 'u':
						oNi.move(0, amount);
						break;
					case 'd':
						oNi.move(0, -amount);
						break;
				}

				// set loop iteration flag and node spread flag
				moved = true;
				again = true;
				break;
			}
		}
		if (!moved) System.out.println("Nothing changed");
	}

	/**
	 * Method to travel through the network, setting flags.
	 * @param ni the NodeInst from which to start traveling.
	 * @param geomSeen the HashSet bit to mark during travel.
	 */
	private static void netTravel(NodeInst ni, Set<Geometric> geomSeen)
	{
		if (geomSeen.contains(ni)) return;
		geomSeen.add(ni);

		for(Iterator<Connection> it = ni.getConnections(); it.hasNext(); )
		{
			Connection con = it.next();
			ArcInst ai = con.getArc();
			if (geomSeen.contains(ai)) continue;
			netTravel(ai.getHeadPortInst().getNodeInst(), geomSeen);
			netTravel(ai.getTailPortInst().getNodeInst(), geomSeen);
		}
	}

	/**
	 * Method to recursively travel along all arcs on a NodeInst.
	 * @param ni the NodeInst to examine.
	 * @param hor true to travel along horizontal arcs; false for vertical.
	 * @param geomSeen the HashSet used to mark nodes that are examined.
	 * This is called from "spread" to propagate along Manhattan
	 * arcs that are in the correct orientation (along the spread line).
	 */
	private static void manhattanTravel(NodeInst ni, boolean hor, Set<Geometric> geomSeen)
	{
		geomSeen.add(ni);
		for(Iterator<Connection> it = ni.getConnections(); it.hasNext(); )
		{
			Connection con = it.next();
			ArcInst ai = con.getArc();
			int angle = ai.getDefinedAngle();
			if (hor)
			{
				// only want horizontal arcs
				if (angle != 0 && angle != 1800) continue;
			} else
			{
				// only want vertical arcs
				if (angle != 900 && angle != 2700) continue;
			}
			int otherEnd = 1 - con.getEndIndex();
			NodeInst other = ai.getPortInst(otherEnd).getNodeInst();
			if (geomSeen.contains(other)) continue;
			manhattanTravel(other, hor, geomSeen);
		}
	}

	/****************************** NODE AND ARC REPLACEMENT ******************************/

	private static class PossibleVariables
	{
		private static Map<PrimitiveNode,List<Variable.Key>> posVarsMap = new HashMap<PrimitiveNode,List<Variable.Key>>();

		private static void add(String varName, PrimitiveNode pn) {
			List<Variable.Key> varKeys = posVarsMap.get(pn);
			if (varKeys == null) {
				varKeys = new ArrayList<Variable.Key>();
				posVarsMap.put(pn, varKeys);
			}
			Variable.Key key = Variable.newKey(varName);
			if (!varKeys.contains(key)) varKeys.add(key);
		}

		static
		{
			add("ATTR_length",       Schematics.tech().transistorNode);
			add("ATTR_length",       Schematics.tech().transistor4Node);
			add("ATTR_width",        Schematics.tech().transistorNode);
			add("ATTR_width",        Schematics.tech().transistor4Node);
			add("ATTR_area",         Schematics.tech().transistorNode);
			add("ATTR_area",         Schematics.tech().transistor4Node);
			add("SIM_spice_model",   Schematics.tech().sourceNode);
			add("SIM_spice_model",   Schematics.tech().transistorNode);
			add("SIM_spice_model",   Schematics.tech().transistor4Node);
			add("SCHEM_meter_type",  Schematics.tech().meterNode);
			add("SCHEM_diode",       Schematics.tech().diodeNode);
			add("SCHEM_capacitance", Schematics.tech().capacitorNode);
			add("SCHEM_resistance",  Schematics.tech().resistorNode);
			add("SCHEM_inductance",  Schematics.tech().inductorNode);
			add("SCHEM_function",    Schematics.tech().bboxNode);
			add("ART_degrees",       Artwork.tech().circleNode);
			add("ART_degrees",       Artwork.tech().thickCircleNode);
		}

		/**
		 * Get an iterator over valid Variable Keys for the primitive node
		 * @param pn the PrimitiveNode to examine.
		 * @return an Iterator over the Variable Keys on the Primitive Node.
		 */
//		public Iterator<Variable.Key> getPossibleVarKeys(PrimitiveNode pn) {
//			List<Variable.Key> varKeys = posVarsMap.get(pn);
//			if (varKeys == null)
//				varKeys = new ArrayList<Variable.Key>();
//			return varKeys.iterator();
//		}

		/**
		 * Method to decide a PrimitiveNode has a Variable key.
		 * @param key the Variable key.
		 * @param pn the PrimitiveNode to examine.
		 * @return true if a Variable key exists on the primitive node.
		 */
		public static boolean validKey(Variable.Key key, PrimitiveNode pn) {
			List<Variable.Key> varKeys = posVarsMap.get(pn);
			if (varKeys == null) return false;
			return varKeys.contains(key);
		}
	}

	/**
	 * Method to replace node "oldNi" with a new one of type "newNp"
	 * and return the new node.  Also removes any node-specific variables.
	 */
	public static NodeInst replaceNodeInst(NodeInst oldNi, NodeProto newNp, PrimitiveNode.Function newFunc, boolean ignorePortNames,
		boolean allowMissingPorts, boolean preseveParameters, EditingPreferences ep) {
        BatchChanges.NodeReplacement replacement = new BatchChanges.NodeReplacement(oldNi, newNp, newFunc, null);
        if (!oldNi.checkReplacement(replacement, ep, ignorePortNames, allowMissingPorts)) {
            return null;
        }
        NodeInst newNi = oldNi.doReplace(replacement, ep, allowMissingPorts, preseveParameters);
        postReplace(newNi, ep);
        return newNi;
    }
    
	/**
	 * Method to replace a batch of nodes "oldNi" with a new one of type "newNp"
	 * and return the new node.  Also removes any node-specific variables.
     * @param replacements a batch of replacement tasks
     * @param allowMissingPorts true to allow replacement to have missing ports and, therefore, delete the arcs that used to be there.
     * @param ep EditingPreferences
     * @throws JobException if replacement failed
	 */
	public static void replaceNodeInsts(Collection<BatchChanges.NodeReplacement> replacements, boolean allowMissingPorts,
		boolean preserveParameters, EditingPreferences ep)
		throws JobException
	{
        EDatabase database = EDatabase.currentDatabase();
        BatchChanges.replaceNodeInsts(database, replacements, allowMissingPorts, preserveParameters, ep);
        for (BatchChanges.NodeReplacement nr: replacements) {
            postReplace(database.getCell(nr.cellId).getNodeById(nr.nodeId), ep);
        }
    }
    
    private static void postReplace(NodeInst newNi, EditingPreferences ep) {
        if (!newNi.isCellInstance())
        {
            // remove variables that make no sense
            for (Iterator<Variable> it = newNi.getVariables(); it.hasNext(); ) {
                Variable var = it.next();
                Variable.Key key = var.getKey();
                if (key != NodeInst.TRACE && !PossibleVariables.validKey(key, (PrimitiveNode)newNi.getProto()))
                {
                    newNi.delVar(var.getKey());
                }
            }
        }

        // now inherit parameters that now do exist
//        inheritAttributes(newNi, ep);
	}

	/****************************** INHERIT ATTRIBUTES ******************************/

	/**
	 * Method to inherit all prototype attributes down to instance "ni".
	 */
	public static void inheritAttributes(NodeInst ni, EditingPreferences ep)
	{
		// ignore primitives
		if (!ni.isCellInstance()) return;
		Cell cell = (Cell)ni.getProto();

		// first inherit parameters from this node's prototype
		if (cell.isIcon())
		{
			for(Iterator<Variable> vIt = cell.getParameters(); vIt.hasNext(); )
			{
				Variable var = vIt.next();
				inheritCellParameter(var, ni, ep);
			}
		}

		// inherit attributes from this node's prototype
		for(Iterator<Variable> it = cell.getVariables(); it.hasNext(); )
		{
			Variable var = it.next();
			if (!var.getTextDescriptor().isInherit()) continue;
			inheritCellAttribute(var, ni);
		}

		// inherit directly from each port's prototype
		for(Iterator<Export> it = cell.getExports(); it.hasNext(); )
		{
			Export pp = it.next();
			inheritExportAttributes(pp, ni, ep);
		}
	}

	/**
	 * Method to add all inheritable export variables from export "pp" on cell "np"
	 * to instance "ni".
	 */
	private static void inheritExportAttributes(Export pp, NodeInst ni, EditingPreferences ep)
	{
		for(Iterator<Variable> it = pp.getVariables(); it.hasNext(); )
		{
			Variable var = it.next();
			if (!var.getTextDescriptor().isInherit()) continue;

			Variable.Key attrKey = var.getKey();

			// see if the attribute is already there
			PortInst pi = ni.findPortInstFromEquivalentProto(pp);
			Variable newVar = pi.getVar(attrKey);
			if (newVar != null) continue;

			// set the attribute
			TextDescriptor td = ep.getPortInstTextDescriptor().withDisplay(false);
			Object value = inheritAddress(pp, var, ep);
			value = Variable.withCode(value, CodeExpression.Code.NONE);
			pi.newVar(attrKey, value, td);
		}
	}

	/**
	 * Method to add inheritable variable "var" from cell "np" to instance "ni".
	 * If "icon" is not NONODEINST, use the position of the variable from it.
	 */
	private static void inheritCellAttribute(Variable var, NodeInst ni)
	{
		// see if the attribute is already there
		Variable.Key key = var.getKey();
		Variable newVar = ni.getVar(key);
		if (newVar != null)
		{
			// make sure visibility is OK
			if (!var.getTextDescriptor().isInterior())
			{
				// parameter should be visible: make it so
				if (!newVar.isDisplay())
				{
					ni.addVar(newVar.withDisplay(true));
				}
			} else
			{
				// parameter not normally visible: make it invisible if it has the default value
				if (newVar.isDisplay())
				{
					if (var.describe(-1).equals(newVar.describe(-1)))
					{
						ni.addVar(newVar.withDisplay(false));
					}
				}
			}
		} else {
			ni.addVar(var);
		}
	}

	/**
	 * Method to add parameter "var" to instance "ni".
	 */
	public static void inheritCellParameter(Variable var, NodeInst ni, EditingPreferences ep)
	{
		// see if the attribute is already there
		Variable.Key key = var.getKey();
		if (ni.isDefinedParameter(key))
		{
			Variable param = ni.getParameter(key);

			// make sure visibility is OK
			if (!var.isInterior())
			{
				// parameter should be visible: make it so
				if (!param.isDisplay())
					ni.addParameter(param.withDisplay(true));
			} else
			{
				// parameter not normally visible: make it invisible if it has the default value
				if (param.isDisplay())
				{
					if (var.describe(-1).equals(param.describe(-1)))
						ni.addParameter(param.withDisplay(false));
				}
			}
		} else
		{
			Cell cell = (Cell)ni.getProto();
			Variable iconVar = cell.getParameter(var.getKey());
			TextDescriptor td = iconVar.getTextDescriptor();
			Rectangle2D bounds = cell.getBounds();
			double xd = td.getXOff() - bounds.getCenterX();
			double yd = td.getYOff() - bounds.getCenterY();
			td = td.withOff(xd, yd);
			Object value = inheritAddress(cell, var, ep);
            ni.addParameter(Variable.newInst(var.getKey(), value, td));
		}
	}

	/**
	 * Helper method to determine the proper value of an inherited Variable.
	 * Normally, it is simply "var.getObject()", but if it is a string with the "++" or "--"
	 * sequence in it, then it indicates an auto-increments/decrements of that numeric value.
	 * The returned object has the "++"/"--" removed, and the original variable is modified.
	 * @param addr the ElectricObject on which this Variable resides.
	 * @param var the Variable being examined.
     * @param ep EditingPreferences
	 * @return the Object in the Variable.
	 */
	private static Object inheritAddress(ElectricObject addr, Variable var, EditingPreferences ep)
	{
		// if it isn't a string, just return its address
		Object obj = var.getObject();
		if (!(obj instanceof String)) return obj;
		if (var.isCode()) return obj;

		String str = (String)obj;
		int plusPlusPos = str.indexOf("++");
		int minusMinusPos = str.indexOf("--");
		if (plusPlusPos < 0 && minusMinusPos < 0) return obj;

		// construct the proper inherited string and increment the variable
		int incrPoint = Math.max(plusPlusPos, minusMinusPos);
		String retVal = str.substring(0, incrPoint) + str.substring(incrPoint+2);

		// increment the variable
		int i = incrPoint-1;
		for( ; i>=0; i--)
			if (!TextUtils.isDigit(str.charAt(i))) break;
		i++;
		int curVal = TextUtils.atoi(str.substring(i));
		if (str.charAt(incrPoint) == '+') curVal++; else curVal--;
		String newIncrString = str.substring(0, i) + curVal + str.substring(incrPoint);
		if (addr instanceof Cell)
		{
			CellGroup cg = ((Cell)addr).getCellGroup();
			cg.updateParam((Variable.AttrKey)var.getKey(), newIncrString, var.getUnit());
		} else
		{
			addr.newVar(var.getKey(), newIncrString, ep);
		}

		return retVal;
	}

	/****************************** LIBRARY CHANGES ******************************/

	/**
	 * This class implement the command to rename a technology.
	 */
	public static class RenameTechnology extends Job
	{
		private Technology tech;
		private String newName;

		public RenameTechnology(Technology tech, String newName)
		{
			super("Renaming " + tech, User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.tech = tech;
			this.newName = newName;
			startJob();
		}

		public boolean doIt() throws JobException
		{
			String oldName = tech.getTechName();
			tech.setTechName(newName);
			System.out.println("Technology '" + oldName + "' renamed to '" + newName + "'");

			// mark all libraries for saving
			for(Iterator<Library> it = Library.getLibraries(); it.hasNext(); )
			{
				Library oLib = it.next();
				if (oLib.isHidden()) continue;
				oLib.setChanged();
			}
			return true;
		}
	}

	/**
	 * This class implement the command to rename a library.
	 */
	public static class RenameLibrary extends Job
	{
		private Library lib;
		private String newName;
		private IdMapper idMapper;

		public RenameLibrary(Library lib, String newName)
		{
			super("Renaming " + lib, User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.lib = lib;
			this.newName = newName;
			startJob();
		}

		public boolean doIt() throws JobException
		{
			String oldName = lib.getName();
			idMapper = lib.setName(newName);
 			if (idMapper == null) return false;
			fieldVariableChanged("idMapper");
			System.out.println("Library '" + oldName + "' renamed to '" + newName + "'");
			return true;
		}

		public void terminateOK() {
			User.fixStaleCellReferences(idMapper);
		}
	}

	/**
	 * Method to implement the "Mark All Libraries for Saving" command.
	 */
	public static void markAllLibrariesForSavingCommand()
	{
		new MarkAllLibraries();
	}

	private static class MarkAllLibraries extends Job
	{
		MarkAllLibraries()
		{
			super("Making all libraries", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			startJob();
		}

		public boolean doIt() throws JobException
		{
			// mark all libraries as "changed"
			for(Iterator<Library> it = Library.getLibraries(); it.hasNext(); )
			{
				Library lib = it.next();
				if (lib.isHidden()) continue;

				// do not mark readable dump files for saving
				String ext = TextUtils.getExtension(lib.getLibFile());
				if (ext.equals("txt")) continue;

				lib.setChanged();
				if (lib.getLibFile() != null && OpenFile.getOpenFileType(lib.getLibFile().getFile(), FileType.JELIB) == FileType.DELIB)
				{
					// set all cells as changed as well
					for (Iterator<Cell> it2 = lib.getCells(); it2.hasNext(); )
					{
						it2.next().lowLevelSetRevisionDate(new Date());
					}
				}
			}
			System.out.println("All libraries now need to be saved");
			return true;
		}
	}

	/**
	 * Method to implement the "Mark All Libraries for Saving" command.
	 */
	public static void markCurrentLibForSavingCommand()
	{
		new MarkCurrentLibForSaving();
	}

	private static class MarkCurrentLibForSaving extends Job
	{
		MarkCurrentLibForSaving()
		{
			super("Making Current Lib", User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			startJob();
		}
		public boolean doIt() throws JobException
		{
			Library lib = Library.getCurrent();
			if (lib.isHidden()) return true;

			// do not mark readable dump files for saving
			String ext = TextUtils.getExtension(lib.getLibFile());
			if (ext.equals("txt")) return true;

			lib.setChanged();
			if (lib.getLibFile() != null && OpenFile.getOpenFileType(lib.getLibFile().getFile(), FileType.JELIB) == FileType.DELIB)
			{
				// set all cells as changed as well
				for (Iterator<Cell> it2 = lib.getCells(); it2.hasNext(); )
				{
					it2.next().lowLevelSetRevisionDate(new Date());
				}
			}

			System.out.println("Library "+lib.getName()+" now needs to be saved");
			return true;
		}
	}

	/**
	 * This class implement the command to repair libraries.
	 */
	public static class CheckAndRepairJob extends Job
	{
		private boolean repair;

		public CheckAndRepairJob(boolean repair)
		{
			super((repair ? "Repair Libraries" : "Check Libraries"), User.getUserTool(), (repair ? Job.Type.CHANGE : Job.Type.SERVER_EXAMINE), null, null, Job.Priority.USER);
			this.repair = repair;
			startJob();
		}

		public boolean doIt() throws JobException
		{
			if (EDatabase.serverDatabase().checkInvariants())
			{
				ErrorLogger errorLogger = ErrorLogger.newInst(repair ? "Repair Libraries" : "Check Libraries");
				int errorCount = 0;
				for(Iterator<Library> it = Library.getLibraries(); it.hasNext(); )
				{
					Library lib = it.next();
					errorCount += lib.checkAndRepair(repair, errorLogger, getEditingPreferences());
				}
				if (errorCount > 0) System.out.println("Found " + errorCount + " errors"); else
					System.out.println("No errors found");
				errorLogger.termLogging(true);
			}
			return true;
		}
	}

	/**
	 * This class implement the command to reload a library
	 */
	public static class ReloadLibraryJob extends Job
	{
		private Library lib;

        public ReloadLibraryJob(Library lib)
		{
			super("Reload Library " + lib.getName(), User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.lib = lib;
			startJob();
		}

		public boolean doIt()
		{
			LibraryFiles.reloadLibrary(getEditingPreferences(), lib);
			return true;
		}
	}

	/****************************** DELETE UNUSED NODES ******************************/

//	public static class RemoveUnusedLayers extends Job
//	{
//		private Library library;
//
//		public RemoveUnusedLayers(Library lib)
//		{
//			super("Remove unused metal layers", null, Type.CHANGE, null, null, Priority.USER);
//			library = lib;
//			startJob();
//		}
//
//		public boolean doIt() throws JobException
//		{
//			// Only one library, the given one
//			if (library != null)
//			{
//				cleanUnusedNodesInLibrary(library);
//				return true;
//			}
//
//			// Checking all
//			for (Iterator<Library> libIter = Library.getLibraries(); libIter.hasNext();)
//			{
//				Library lib = libIter.next();
//				cleanUnusedNodesInLibrary(lib);
//			}
//			return true;
//		}
//
//		private void cleanUnusedNodesInLibrary(Library lib)
//		{
//			int action = -1;
//			List<Geometric> list = new ArrayList<Geometric>();
//
//			for (Iterator<Cell> cellsIter = lib.getCells(); cellsIter.hasNext();)
//			{
//				Cell cell = cellsIter.next();
//				if (cell.getView() != View.LAYOUT) continue; // only layout
//				list.clear();
//				Technology tech = cell.getTechnology();
//
//				for (int i = 0; i < cell.getNumArcs(); i++)
//				{
//					ArcInst ai = cell.getArc(i);
//					ArcProto ap = ai.getProto();
//					if (ap.isNotUsed())
//						list.add(ai);
//				}
//				for (int i = 0; i < cell.getNumNodes(); i++)
//				{
//					NodeInst ni = cell.getNode(i);
//					tech.cleanUnusedNodesInLibrary(ni, list);
//				}
//				if (action != 3 && list.size() > 0)
//				{
//					String [] options = {"Yes", "No", "Cancel", "Yes to All"};
//
//					action = JOptionPane.showOptionDialog(TopLevel.getCurrentJFrame(),
//							"Remove unused nodes in " + cell.libDescribe(), "Warning",
//							JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
//							null, options, options[0]);
//					if (action == 2) return; // cancel
//				}
//				if (action != 1) // 1 is No to this local modification
//				{
//					System.out.println("Removing " + list.size() + " unused nodes in " + cell.libDescribe());
//					 eraseObjectsInList(cell, list);
//				}
//			}
//		}
//	}

	/****************************** DETERMINE ABILITY TO MAKE CHANGES ******************************/

	/**
	 * Method to test whether a NodeInst can be modified in a cell.
	 * Throws an exception if not.
	 * @param cell the Cell in which the NodeInst resides.
	 * @param item the NodeInst (may be null).
	 * @param giveError true to print an error message if the modification is disallowed.
	 * @param allowInstanceChange true to allow a change to an instance when instances are locked
	 * (this allows exports to be created and deleted).
	 */
	public static void testEditable(Cell cell, NodeInst item, boolean giveError, boolean allowInstanceChange)
		throws CantEditException
	{
		// if an instance is specified, check it
		if (item != null)
		{
			if (item.isLocked())
			{
				CantEditException e = new CantEditException();
				e.lockedNode = item;
				throw e;
			}
			boolean complexNode = false;
			if (!item.isCellInstance())
			{
				// see if a primitive is locked
				if (((PrimitiveNode)item.getProto()).isLockedPrim() &&
					User.isDisallowModificationLockedPrims())
				{
					CantEditException e = new CantEditException();
					e.lockedPrim = item;
					throw e;
				}
				PrimitiveNode.Function fun = item.getFunction();
				if (!fun.isPin() && !fun.isContact() &&
					fun != PrimitiveNode.Function.NODE && fun != PrimitiveNode.Function.CONNECT)
						complexNode = true;
			} else
			{
				// see if this type of cell is locked
				complexNode = true;
				if (!allowInstanceChange && cell.isInstancesLocked())
				{
					CantEditException e = new CantEditException();
					e.lockedInstances = cell;
					e.lockedExample = item;
					throw e;
				}
			}
			if (complexNode)
			{
				if (User.isDisallowModificationComplexNodes())
				{
					CantEditException e = new CantEditException();
					e.lockedComplex = item;
					throw e;
				}
			}
		}

		// check for general changes to the cell
		if (cell.isAllLocked())
		{
			CantEditException e = new CantEditException();
			e.lockedAll = cell;
			e.lockedExample = item;
			throw e;
		}
	}

	/**
	 * Method to tell whether a NodeInst can be modified in a cell.
	 * WARNING: method may change the database if the user disables a cell lock,
	 * so method must be called inside of a Change job.
	 * @param cell the Cell in which the NodeInst resides.
	 * @param item the NodeInst (may be null).
	 * @param giveError true to print an error message if the modification is disallowed.
	 * @param allowInstanceChange true to allow a change to an instance when instances are locked
	 * (this allows exports to be created and deleted).
	 * @param insideJob true if this is being run inside of a job.
	 * @return positive if the edit CANNOT be done.
	 * Return negative if the edit CANNOT be done and the overall operation should be canceled.
	 * Return zero if the operation CAN be done.
	 */
	public static int cantEdit(Cell cell, NodeInst item, boolean giveError, boolean allowInstanceChange, boolean insideJob)
	{
		String [] options = {"Yes", "No", "Always", "Cancel"};
		// if an instance is specified, check it
		if (item != null)
		{
			if (item.isLocked())
			{
				if (!giveError) return 1;
				int ret = JOptionPane.showOptionDialog(TopLevel.getCurrentJFrame(),
					"Changes to locked " + item + " are disallowed.  Change anyway?",
					"Allow changes", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
					null, options, options[1]);
				if (ret == 1) return 1;
				if (ret == 2)
				{
					if (insideJob) item.clearLocked(); else
						new ClearNodeLocked(item);
				}
				if (ret == 3 || ret == -1) return -1;  // -1 represents ESC or cancel
			}
			boolean complexNode = false;
			if (!item.isCellInstance())
			{
				// see if a primitive is locked
				if (((PrimitiveNode)item.getProto()).isLockedPrim() &&
					User.isDisallowModificationLockedPrims())
				{
					if (!giveError) return 1;
					int ret = JOptionPane.showOptionDialog(TopLevel.getCurrentJFrame(),
						"Changes to locked primitives (such as " + item + ") are disallowed.  Change anyway?",
						"Allow changes", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
						null, options, options[1]);
					if (ret == 1) return 1;
					if (ret == 2) User.setDisallowModificationLockedPrims(false);
					if (ret == 3) return -1;
				}
				PrimitiveNode.Function fun = item.getFunction();
				if (!fun.isPin() && !fun.isContact() &&
					fun != PrimitiveNode.Function.NODE && fun != PrimitiveNode.Function.CONNECT)
						complexNode = true;
			} else
			{
				// see if this type of cell is locked
				complexNode = true;
				if (!allowInstanceChange && cell.isInstancesLocked())
				{
					if (!giveError) return 1;
					int ret = JOptionPane.showOptionDialog(TopLevel.getCurrentJFrame(),
						"Modification of instances in " + cell + " is disallowed.  You cannot move " + item +
						".  Change anyway?",
						"Allow changes", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
						null, options, options[1]);
					if (ret == 1) return 1;
					if (ret == 2)
					{
						if (insideJob) cell.clearInstancesLocked(); else
							new ClearCellLocked(cell, false);
					}
					if (ret == 3) return -1;
				}
			}
			if (complexNode)
			{
				if (User.isDisallowModificationComplexNodes())
				{
					if (!giveError) return 1;
					int ret = JOptionPane.showOptionDialog(TopLevel.getCurrentJFrame(),
						"Changes to complex nodes (such as " + item + ") are disallowed.  Change anyway?",
						"Allow changes", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
						null, options, options[1]);
					if (ret == 1) return 1;
					if (ret == 2) User.setDisallowModificationComplexNodes(false);
					if (ret == 3) return -1;
				}
			}
		}

        if (cell == null)
        {
            // operation should be canceled since the cell is null
            return -1;
        }

        // check for general changes to the cell
		if (cell.isAllLocked())
		{
			if (!giveError) return 1;
			int ret = JOptionPane.showOptionDialog(TopLevel.getCurrentJFrame(),
				"Modification of " + cell + " is disallowed.  Change "+((item == null)? "" : item.toString())+" anyway?",
				"Allow changes", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
				null, options, options[1]);
			if (ret == 1) return 1;
			if (ret == 2)
			{
				if (insideJob) cell.clearAllLocked(); else
					new ClearCellLocked(cell, true);
			}
			if (ret == 3) return -1;
		}
		return 0;
	}

	/**
	 * This class clears a node lock bit in a Job.
	 */
	public static class ClearNodeLocked extends Job
	{
		private NodeInst ni;

		public ClearNodeLocked(NodeInst ni)
		{
			super("Clear locked state of " + ni.describe(false), User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.ni = ni;
			startJob();
		}

		public boolean doIt()
		{
			ni.clearLocked();
			return true;
		}
	}

	/**
	 * This class clears a cell lock bit in a Job.
	 */
	public static class ClearCellLocked extends Job
	{
		private Cell cell;
		private boolean all;

		public ClearCellLocked(Cell cell, boolean all)
		{
			super("Clear locked state of " + cell.getName(), User.getUserTool(), Job.Type.CHANGE, null, null, Job.Priority.USER);
			this.cell = cell;
			this.all = all;
			startJob();
		}

		public boolean doIt()
		{
			if (all) cell.clearAllLocked(); else
				cell.clearInstancesLocked();
			return true;
		}
	}

    /****************************** Make Cell Annotation Job ******************************/
    public static class MakeCellAnnotationJob extends Job
    {
    	static final long serialVersionUID = 0;

		private transient EditWindow_ wnd;
        private Cell cell;
        private String newAnnotation;
        private Variable.Key key;

        public static void makeAnnotationMenuCommand(Tool tool, Variable.Key key, String newAnnotation)
        {
            UserInterface ui = Job.getUserInterface();
            EditWindow_ wnd = ui.needCurrentEditWindow_();
            if (wnd == null) return;
            Cell cell = ui.needCurrentCell();
            if (cell == null) return;
		    new MakeCellAnnotationJob(wnd, cell, tool, key, newAnnotation);
        }

        private MakeCellAnnotationJob(EditWindow_ wnd, Cell cell, Tool tool, Variable.Key k, String annotation)
        {
            super("Make Cell Annotation", tool, Job.Type.CHANGE, null, null, Job.Priority.USER);
            this.wnd = wnd;
            this.cell = cell;
            newAnnotation = annotation;
            key = k;
            startJob();
        }
		@Override
        public boolean doIt() throws JobException {
        	addAnnotation(cell, key, newAnnotation, null, null, getEditingPreferences());
        	return true;
        }
		@Override
        public void terminateOK() {
        	wnd.clearHighlighting();
			wnd.addHighlightText(cell, cell, key);
			wnd.finishedHighlighting();
        }

		public static void addAnnotation(Cell c, Variable.Key k, String annotation, EditingPreferences ep)
		{
			addAnnotation(c, k, annotation, null, null, ep);
		}
		
        public static void addAnnotation(Cell c, Variable.Key k, String annotation, ERectangle rect, 
        		TextDescriptor.Position pos, EditingPreferences ep)
        {
            Variable var = c.getVar(k);
            
            if (var == null) {
                String [] initial = new String[1];
                initial[0] = annotation;
                TextDescriptor td = ep.getCellTextDescriptor().withInterior(true).withDispPart(TextDescriptor.DispPos.NAMEVALUE);
                if (rect != null)
                	td = td.withOff(rect.getCenterX(), -rect.getCoordMaxY().getLambda()).withPos(TextDescriptor.Position.DOWN);
                if (pos != null)
                	td = td.withPos(pos);
                var = c.newVar(k, initial, td);
                Job.error(var==null, "couldn't create" + k + " annotation");
            } else {
                Object oldObj = var.getObject();
                if (oldObj instanceof String) {
                    /* Groan! Menu command always creates attributes as arrays of strings.
                     * However, if user edits a single line attribute then dialog box
                     * converts it back into a String.  Be prepared to convert it back into an array*/
                    oldObj = new String[] {(String)oldObj};
                }
                Job.error(!(oldObj instanceof String[]), k + " annotation not String[]");
                String[] oldVal = (String[]) oldObj;
                TextDescriptor td = var.getTextDescriptor();

                int newLen = oldVal.length+1;
                String[] newVal = new String[newLen];
                for (int i=0; i<newLen-1; i++) newVal[i]=oldVal[i];
                newVal[newLen-1] = annotation;
                var = c.newVar(k, newVal, td);
            }
        }
    }
}
