Package org.openquark.gems.client

Source Code of org.openquark.gems.client.TableTopPanel$KeyStrokeHandler

/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*     * Redistributions of source code must retain the above copyright notice,
*       this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of Business Objects nor the names of its contributors
*       may be used to endorse or promote products derived from this software
*       without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/


/*
* TableTopPanel.java
* Creation date: Dec 18th 2002
* By: Ken Wong
*/
package org.openquark.gems.client;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DragSource;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JLayeredPane;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.LayoutFocusTraversalPolicy;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;

import org.openquark.cal.compiler.FieldName;
import org.openquark.cal.compiler.ModuleTypeInfo;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.cal.services.GemEntity;
import org.openquark.cal.valuenode.ValueNode;
import org.openquark.gems.client.DisplayedGem.DisplayedPart;
import org.openquark.gems.client.DisplayedGem.DisplayedPartBody;
import org.openquark.gems.client.DisplayedGem.DisplayedPartConnectable;
import org.openquark.gems.client.DisplayedGem.DisplayedPartInput;
import org.openquark.gems.client.DisplayedGem.DisplayedPartOutput;
import org.openquark.gems.client.Gem.PartConnectable;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.GemCutter.GUIState;
import org.openquark.gems.client.utilities.ExtendedUndoManager;
import org.openquark.gems.client.utilities.ExtendedUndoableEditSupport;
import org.openquark.gems.client.utilities.MouseClickDragListener;
import org.openquark.gems.client.utilities.MouseClickDragListener.DragMode;
import org.openquark.gems.client.valueentry.ValueEditorAdapter;
import org.openquark.gems.client.valueentry.ValueEditorContext;
import org.openquark.gems.client.valueentry.ValueEditorEvent;
import org.openquark.gems.client.valueentry.ValueEditorHierarchyManager;
import org.openquark.gems.client.valueentry.ValueEditorManager;
import org.openquark.gems.client.valueentry.ValueEntryPanel;
import org.openquark.util.UnsafeCast;
import org.openquark.util.ui.UIUtilities;


/**
* This class represents the JPanel that is responsible for displaying the contents on the tabletop.
* Most of the work is done in the TableTop class, and so this class serves primarily as a UI class.
*
* Note: many functions in this class were moved from the TableTop class.
*
* @author Ken Wong
*/
public class TableTopPanel extends JLayeredPane implements Scrollable {
   
    /*
     * Static members -------------------------------------------------------
     */
   
    private static final long serialVersionUID = 3138348301033889627L;

    /** Whether the system supports drag images natively (otherwise we must render our own). */
    static final boolean DRAG_IMAGE_SUPPORTED = DragSource.isDragImageSupported();
   
    /** The minimum horizontal spacing between gems that are placed with the automatic placement algorithm. */
    static final int SEARCH_HORIZONTAL_SPACING = 8;

    /*
     * Cursors and images
     */
    static final ImageIcon blankImageIconSmall;
    static final ImageIcon burnImageIconSmall;
    static final ImageIcon burnNoParkImageIconSmall;
    static final ImageIcon burnQuestionImageIconSmall;
    static final ImageIcon connectImageIconSmall;
    static final ImageIcon connectNoParkImageIconSmall;
    static final ImageIcon collectorImageIconSmall;
    static final ImageIcon emitterImageIconSmall;
    static final ImageIcon reflectorImageIconSmall;
    static final ImageIcon valueGemImageIconSmall;

    public static final Cursor burnCursor;
    public static final Cursor burnNoParkCursor;
    public static final Cursor burnQuestionCursor;
    public static final Cursor cloneGemCursor;
    public static final Cursor connectCursor;
    public static final Cursor connectNoParkCursor;
   
    // Initialize the small images and the cursors
    static {
        // First get some useful reference objects.
        Toolkit tk = Toolkit.getDefaultToolkit();
        Dimension bestsize = tk.getBestCursorSize(32,32);
       
        // Get the images.  Use ImageIcon to ensure that they're loaded.
        ImageIcon burnImageIcon          = new ImageIcon(TableTop.class.getResource("/Resources/cursorBurn.gif"));
        ImageIcon burnNoParkImageIcon    = new ImageIcon(TableTop.class.getResource("/Resources/cursorBurnNoPark.gif"));
        ImageIcon burnQuestionImageIcon  = new ImageIcon(TableTop.class.getResource("/Resources/cursorBurnQuestion.gif"));
        ImageIcon cloneCursorImageIcon   = new ImageIcon(GemCutter.class.getResource("/Resources/cursorCloneGem.gif"));
        ImageIcon connectImageIcon       = new ImageIcon(GemCutter.class.getResource("/Resources/cursorConnect.gif"));
        ImageIcon connectNoParkImageIcon = new ImageIcon(GemCutter.class.getResource("/Resources/cursorConnectNoPark.gif"));
        ImageIcon collectorImageIcon     = new ImageIcon(GemCutter.class.getResource("/Resources/collector.gif"));
        ImageIcon emitterImageIcon       = new ImageIcon(GemCutter.class.getResource("/Resources/emitter.gif"));
        ImageIcon reflectorImageIcon     = new ImageIcon(GemCutter.class.getResource("/Resources/reflector.gif"));
        ImageIcon valueGemImageIcon      = new ImageIcon(GemCutter.class.getResource("/Resources/constant.gif"));
       
        System.setProperty("gemcutter.photolook", "true");
       
        // These small image icons are used by Intellicut
        int smallWidth = 12;
        int smallHeight = 12;
        blankImageIconSmall         = new ImageIcon(new BufferedImage(smallWidth, smallHeight, BufferedImage.TYPE_INT_ARGB));
        burnImageIconSmall          = new ImageIcon(burnImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH));
        burnNoParkImageIconSmall    = new ImageIcon(burnNoParkImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH));
        burnQuestionImageIconSmall  = new ImageIcon(burnQuestionImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH));
        connectImageIconSmall       = new ImageIcon(connectImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH));
        connectNoParkImageIconSmall = new ImageIcon(connectNoParkImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH));
        collectorImageIconSmall     = new ImageIcon(collectorImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH));
        emitterImageIconSmall       = new ImageIcon(UIUtilities.cropImage(UIUtilities.shiftImage(new ImageIconemitterImageIcon.getImage().getScaledInstance(smallWidth - 1, smallHeight - 1, Image.SCALE_SMOOTH)).getImage(), 2, 3), 1, 0, 0, 2));
        reflectorImageIconSmall     = new ImageIcon(UIUtilities.cropImage(UIUtilities.shiftImage(new ImageIcon(reflectorImageIcon.getImage().getScaledInstance(smallWidth - 1, smallHeight - 2, Image.SCALE_SMOOTH)).getImage(), 1, 3), 0, 0, 0, 1));
        valueGemImageIconSmall      = new ImageIcon(valueGemImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH));

        // Do we support custom cursors?
        if (bestsize.width > 15) {
            // declare the hotspot for the cursor, some useful constants.
            // hotSpot values must be less than the Dimension returned by getBestCursorSize
            Point burnHotSpot = new Point(8, 15);
            Point connectHotSpot = new Point(1, 1);
           
            // Scale the images to the cursor size.
            BufferedImage scaledBurnCursorImage = GemCutterPaintHelper.getResizedImage(burnImageIcon.getImage(), bestsize);
            BufferedImage scaledBurnNoParkCursorImage = GemCutterPaintHelper.getResizedImage(burnNoParkImageIcon.getImage(), bestsize);
            BufferedImage scaledCloneCursorImage = GemCutterPaintHelper.getResizedImage(cloneCursorImageIcon.getImage(), bestsize);
            BufferedImage scaledBurnQuestionCursorImage = GemCutterPaintHelper.getResizedImage(burnQuestionImageIcon.getImage(), bestsize);
            BufferedImage scaledConnectCursorImage = GemCutterPaintHelper.getResizedImage(connectImageIcon.getImage(), bestsize);
            BufferedImage scaledConnectNoParkCursorImage = GemCutterPaintHelper.getResizedImage(connectNoParkImageIcon.getImage(), bestsize);

            // define the cursors
            burnCursor = tk.createCustomCursor(scaledBurnCursorImage, burnHotSpot, "BurnCursor");
            burnNoParkCursor = tk.createCustomCursor(scaledBurnNoParkCursorImage, burnHotSpot, "BurnNoParkCursor");
            burnQuestionCursor = tk.createCustomCursor(scaledBurnQuestionCursorImage, burnHotSpot, "BurnQuestionCursor");
            cloneGemCursor = tk.createCustomCursor(scaledCloneCursorImage, connectHotSpot, "CloneGemCursor");
            connectCursor = tk.createCustomCursor(scaledConnectCursorImage, connectHotSpot, "ConnectCursor");
            connectNoParkCursor = tk.createCustomCursor(scaledConnectNoParkCursorImage, connectHotSpot, "ConnectNoParkCursor");
           
        } else {
            // Platform don't support custom cursors.
            // Linux is probably the only platform that doesn't support custom cursors.
            burnCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
            burnNoParkCursor = DragSource.DefaultLinkNoDrop;
            burnQuestionCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
            cloneGemCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
            connectCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
            connectNoParkCursor = DragSource.DefaultLinkNoDrop;
        }
    }
   
    /** Our own reference to gemCutter. */
    private final GemCutter gemCutter;
 
    /** Our own reference to tableTop. */
    private final TableTop tableTop;
   
    /** The handler for mouse events in edit mode. */
    private final TableTopMouseHandler tableTopMouseHandler;
   
    /** The handler for mouse event in run mode. */
    private final RunModeMouseHandler runModeMouseHandler;

    /** The painter for the TableTop. */
    private final TableTopGemPainter gemPainter;
   
    /**Map from value gem to the value entry panel used to edit its value. */
    private final Map<ValueGem, ValueEntryPanel> valueGemPanelMap;
   
    /**
     * The popup menu currently being shown by the table top. This is either
     * the table top popup, gem part popup, gem popup or run mode popup.
     */
    private JPopupMenu currentPopupMenu = null;
   
    /** The location the popup menu has displayed at or will be displayed at. */
    private Point currentPopupLocation = new Point();       // ensure this is never null..
   
    /** Whether or not a popup menu is allowed to be shown. */
    private boolean popupShouldShow = true;
   
    /** The location at which gems should be pasted if paste was invoked from a popup menu. */
    private Point pasteLocation = null;
   
    /** The table top background image. */
    private BufferedImage backgroundImage = null;
   
    /** The x offset of the background image used to create the illusion of 'scrolling' */
    private int backgroundImageOriginOffsetX = 0;
   
    /** The y offset of the background image used to create the illusion of 'scrolling' */
    private int backgroundImageOriginOffsetY = 0;
   
    /** The DisplayedGem that is used as the anchor for selections (mouse or keyboard) using the SHIFT key. */
    private DisplayedGem shiftSelectionAnchorGem = null;

    /**
     * Flag to indicate that a paint is occurring, and not to do any drawings on the TableTop
     * until the full paint is finished (flag reset).
     */
    private int isPainting;
   

    /**
     * Draw action enum pattern.
     * Creation date: (31/08/2001 10:58:43 AM)
     * @author Edward Lam
     */
    static final class DrawAction {
        static final DrawAction DRAW   = new DrawAction ();
        static final DrawAction UNDRAW = new DrawAction ();
        static final DrawAction REDRAW = new DrawAction ();

        /**
         * Constructor for a draw action
         */
        private DrawAction() {
        }
    }


    /**
     * Select Mode enum pattern to describe the different selection modes that can
     * arise while selecting gems with the mouse or keyboard.
     * Creation date: (12/04/01 11:21:43 AM)
     * @author Edward Lam
     */
    static final class SelectMode {
        static final SelectMode REPLACE_SELECT = new SelectMode (); // Regular
        static final SelectMode TOGGLE = new SelectMode ();         // Control meta
        static final SelectMode SELECT = new SelectMode ();         // Shift meta

        /**
         * Constructor for a drag select mode
         */
        private SelectMode() {
        }
    }

    /**
     * An action used to add a new reflector for a collector to the table top.
     * This is used by the 'Add Other Emitter' menu item for the non-gem
     * table top menu.
     */
    private class AddReflectorAction extends AbstractAction {
       
        private static final long serialVersionUID = -7296061875095016427L;
        private final CollectorGem collector;
       
        public AddReflectorAction(CollectorGem collector) {
            super(collector.getUnqualifiedName());
            this.collector = collector;
        }
       
        public void actionPerformed(ActionEvent evt) {
            DisplayedGem dGem = tableTop.createDisplayedReflectorGem(currentPopupLocation, collector);
            ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();
           
            editSupport.beginUpdate();
            tableTop.doAddGemUserAction(dGem, currentPopupLocation);
            editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
            editSupport.endUpdate();
        }
    }

    /**
     * An editable text field that accepts valid CAL identifiers for variable names
     * Creation date: (10/29/01 4:04:00 PM)
     * @author Edward Lam
     */
    class EditableGemNameField extends EditableIdentifierNameField.VariableName {

        private static final long serialVersionUID = -500686040771757761L;

        /** The gem to which this refers */
        private final Gem gem;

        /** The name of the before the name gets changed. */
        private String oldName;

        /** Keeps track of whether this component has been removed from the tableTop */ // is there a better way??
        private boolean removed = false;

        /** The undo manager for this text field. */
        private ExtendedUndoManager undoManager;
       
        /**
         * Constructor for a new EditableGemNameField.
         * @param initialText the text initially displayed in this field
         * @param gem the gem to edit the name for
         */
        EditableGemNameField(String initialText, Gem gem) {
            super();
            this.gem = gem;            // we have to set this before we call isValidName() ...
            initialize(initialText);
        }
       
        /**
         * Initializes this text field
         * @param initialText
         */
        private void initialize(String initialText) {
           
            oldName = gem instanceof CollectorGem ? ((CollectorGem)gem).getUnqualifiedName() : ((CodeGem)gem).getUnqualifiedName();
           
            // Hopefully, the initial text is a valid name!
            if (!isValidName(initialText)) {
                throw new IllegalArgumentException("Programming Error: attempting to initialize the name of a variable with invalid name: " + initialText);
            }
           
            // set the initial text
            setInitialText(initialText);
            setText(initialText);
           
            // set the font of the text
            setFont(gem instanceof CodeGem ? GemCutterPaintHelper.getTitleFont() : GemCutterPaintHelper.getBoldFont());
           
            // update the size of the text area to reflect the size of the text
            updateSize();
           
            // starts out with all text selected
            selectAll();
           
            // set up the undo manager
            undoManager = new ExtendedUndoManager();
            getDocument().addUndoableEditListener(undoManager);
           
            // moving focus away commits the text entered and closes this component
            addFocusListener(new FocusAdapter(){
                public void focusLost(FocusEvent e) {
                    commitText();
                }
            });
           
            // intercept some key events           
            addKeyListener(new KeyAdapter(){
                public void keyPressed(KeyEvent e) {
                    int keyCode = e.getKeyCode();
                   
                    // pressing "ESC" cancels text entry and closes this component
                    if (keyCode == KeyEvent.VK_ESCAPE) {
                        cancelEntry();
                    }
                   
                    KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
                   
                    // handle undo and redo
                    if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_UNDO)) {
                        if (undoManager.canUndo()) {
                            undoManager.undo();
                            textChanged();
                        }
                        e.consume();
                       
                    } else if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_REDO)) {
                        if (undoManager.canRedo()) {
                            undoManager.redo();
                            textChanged();
                        }
                        e.consume();
                       
                    } else if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_ARRANGE_GRAPH) ||
                            keyStroke.equals(GemCutterActionKeys.ACCELERATOR_NEW)) {
                       
                        // We have to intercept accelerators for these so that the GemCutter
                        // doesn't get screwed up when the text field doesn't match the action result.
                        e.consume();
                    }
                }
            });
           
            // ensure the cursor is visible when it moves
            addCaretListener(new CaretListener(){
                public void caretUpdate(CaretEvent e){
                    // just ensure the caret is visible
                    scrollCaretToVisible();
                }
            });
        }

        /**
         * Cancel text entry (press "ESC" ..)
         */
        protected void cancelEntry(){
            // Revert to the last valid name
            setText(getInitialText());
           
            // What to do, what to do..
            textCommittedInvalid();
        }

        /**
         * Close this window (if not already gone..)
         */
        synchronized void closeField(){
            // check if we've removed this already
            if (!removed) {
                removed = true;
                TableTopPanel.this.remove(this);
                TableTopPanel.this.repaint(EditableGemNameField.this.getBounds());

                // we have to do this or else you can just keep typing (..!)
                setEnabled(false);
               
                // Update the tabletop for the new gem graph state.
                //  Among other things, this will ensure arg name disambiguation with respect to the new collector name.
                tableTop.updateForGemGraph();
            }
            // trigger a focusLost() on this component if it had focus
            TableTopPanel.this.requestFocus();
        }

        /**
         * If this has been placed in the tabletop, make sure the caret is visible
         */
        void scrollCaretToVisible(){
            if (TableTopPanel.this.isAncestorOf(this)) {
                int dotPos = getCaret().getDot();
                try {
                    Rectangle caretRect = modelToView(dotPos);
                    caretRect.width += 1;
                    Rectangle convertedRect =
                        SwingUtilities.convertRectangle(this, caretRect, TableTopPanel.this);
                    TableTopPanel.this.scrollRectToVisible(convertedRect);
                   
                } catch (BadLocationException e) {
                    // Nowhere to scroll.  Oh well.
                }
            }
        }

        /**
         * Notify that the text of this text field has changed.  Called upon insertUpdate() and remove() completion.
         * Eg. If the current result is not valid, maybe do something about it (like warn the user somehow..)
         */
        protected void textChanged(){
            super.textChanged();
           
            tableTop.resizeForGems();

            // ensure the caret is visible
            scrollCaretToVisible();
           
        }

        /**
         * Returns whether a name is a valid name for this field
         * @param name the name to check for validity
         */
        protected boolean isValidName(String name){
            if (!(super.isValidName(name))) {
                return false;
            }

            return tableTop.isAvailableCodeOrCollectorName(name, gem);
        }

        /**
         * Take appropriate action if the result of the text change is invalid.
         */
        protected void textChangeInvalid(){
            // Signal the user.
            setForeground(Color.lightGray);

            // update the gem name to display the new text (despite being invalid)
            updateGemName(getText());

            // set a tooltip saying that the text is invalid
            String text = GemCutter.getResourceString("ToolTip_InvalidVariableName");
            String[] lines = ToolTipHelpers.splitTextIntoLines(text, 300, getFont(), ((Graphics2D)TableTopPanel.this.getGraphics()).getFontRenderContext());
            text = "<html>" + lines [0];
            for (int i = 1; i < lines.length; i++) {
                text += "<br>" + lines[i];
            }
            setToolTipText(text + "</html>");

            // update the text field to reflect the new size of the text
            updateSize();
        }

        /**
         * Take appropriate action if the result of the text change is valid.
         */
        protected void textChangeValid(){
            // do validation checking - paint text colors differently depending on the result
            setForeground(Color.black);

            // update the gem name to display the new text
            updateGemName(getText());

            // clear any tooltip saying that the text is invalid
            setToolTipText(null);
           
            // update the text field to reflect the new size of the text
            updateSize();
        }

        /**
         * Take appropriate action if the text committed is valid.
         */
        protected void textCommittedInvalid(){
            // the text is already reverted.  Update the gem name to reflect this.
            String revertedText = getText();
            updateGemName(revertedText);
           
            // close this component
            closeField();
        }

        /**
         * Take appropriate action if the text committed is valid.
         */
        protected void textCommittedValid(){
            // Update the gem name.
            String committedText = getText();
            updateGemName(committedText);

            // close this component
            closeField();

            // the text size wouldn't change on commit so no need to repaint let gems
           
            // Notify the undo manager of the name change, if any
            String newName = gem instanceof CollectorGem ? ((CollectorGem)gem).getUnqualifiedName() : ((CodeGem)gem).getUnqualifiedName();
            if (!newName.equals(oldName)) {
                if (gem instanceof CollectorGem) {
                    tableTop.getUndoableEditSupport().postEdit(new UndoableChangeCollectorNameEdit(tableTop, (CollectorGem)gem, oldName));
                   
                } else if (gem instanceof CodeGem) {
                    tableTop.getUndoableEditSupport().postEdit(new UndoableChangeCodeGemNameEdit(tableTop, (CodeGem)gem, oldName));
                }
            }
        }

        /**
         * Update the name of the gem represented by this text field.
         * @param newName String the new name for the let gem
         */
        private void updateGemName(String newName){
            if (gem instanceof CodeGem) {
                tableTop.renameCodeGem((CodeGem)gem, newName);
            } else {
                ((CollectorGem)gem).setName(newName);
            }
        }

        /**
         * Update the size of this field.
         */
        private void updateSize(){
            Insets insets = getInsets();

            // The X dimension is based on the size of the text for the name (plus some margins)
            FontMetrics fm = getFontMetrics(getFont());

            // Calculate width and height
            int newWidth = fm.stringWidth(getText()) + insets.right + insets.left + 1;
            int newHeight = fm.getHeight();

            setSize(new Dimension(newWidth, newHeight));
        }
    }


    /**
     * Listener which is invoked when the keyboard is used..
     * Creation date: (12/09/2001 9:37:19 AM)
     * @author Edward Lam
     */
    class KeyStrokeHandler extends KeyAdapter {
       
        /**
         * Called when a key is released
         * @param evt
         */
        public void keyReleased(KeyEvent evt) {
            // If the user is dragging, and Ctrl is released, then we want to get out of the Ctrl-drag mode
            if (evt.getKeyCode() == KeyEvent.VK_CONTROL) {
                if (tableTopMouseHandler.isGemDragging()) {
                    tableTopMouseHandler.setDragMode(TableTopDragMode.GEMDRAGGING);
                }
            }
        }
       
        /**
         * Called when a key is pressed. 
         * @param evt KeyEvent the related KeyEvent
         */
        public void keyPressed(KeyEvent evt) {

            // Only pay attention to this key event if none of the popup menus are open
            if (currentPopupMenu != null && currentPopupMenu.isVisible()) {
                return;
            }
           
            NavigationDirection navDirection = null;
            DisplayedGem focusedGem = tableTop.getFocusedDisplayedGem();

            switch (evt.getKeyCode()) {
           
            case KeyEvent.VK_CONTROL:
                if (tableTopMouseHandler.isGemDragging()) {
                    tableTopMouseHandler.setDragMode(TableTopDragMode.CTRLDRAGGING);
                }
                break;
               
            case KeyEvent.VK_UP:
                navDirection = NavigationDirection.UP;
                break;
               
            case KeyEvent.VK_DOWN:
                navDirection = NavigationDirection.DOWN;
                break;
               
            case KeyEvent.VK_LEFT:
                navDirection = NavigationDirection.LEFT;
                break;
               
            case KeyEvent.VK_RIGHT:
                navDirection = NavigationDirection.RIGHT;
                break;
               
            case KeyEvent.VK_SPACE:
                if (focusedGem != null) {
                   
                    // What we do depends on the modifiers
                    if (evt.isControlDown() && evt.isShiftDown()) {
                       
                        // Singleton select the focused Gem when both CTRL + SHIFT are used with the SPACE
                        // and update the shift selection anchor Gem
                        tableTop.selectDisplayedGem(focusedGem, true);
                        shiftSelectionAnchorGem = focusedGem;
                       
                    } else if (evt.isControlDown() || !tableTop.isSelected(focusedGem)) {
                       
                        // Toggle the selection state of the focused gem (if there is one)
                        // and make the focused gem the new selection anchor.
                        tableTop.toggleSelected(focusedGem);
                        shiftSelectionAnchorGem = focusedGem;
                    }
                }
                return;
               
            case KeyEvent.VK_ESCAPE:
                // todoSN - This may be temporary, although it may be good to clear the selection with Escape.
                //            Wait to see what happens once proper focus switching is working in the GemCutter.
                // Clear any selections and return focus to the TableTop
                if (!tableTopMouseHandler.isUsefulDragMode(tableTopMouseHandler.getDragMode())) {
                    tableTop.selectDisplayedGem(null, true);
                    shiftSelectionAnchorGem = null;
                    tableTop.setFocusedDisplayedGem(null);
                    requestFocus();
                    return;
                } else {
                    Graphics2D g2d = (Graphics2D)getGraphics();
                    tableTopMouseHandler.drawDragGhost(TableTopPanel.DrawAction.UNDRAW, TableTopPanel.SelectMode.REPLACE_SELECT, g2d);
                    g2d.dispose();
                    tableTopMouseHandler.setDragMode(TableTopDragMode.USELESS);
                    break;
                }
               
            default:
                return;
            }
           
            // If the navigation direction is not null and there is a
            // focused Gem we know an arrow was pressed for navigation
            if (navDirection != null && focusedGem != null) {

                // Get the next gem to gain focus
                DisplayedGem nextGem = tableTop.findNearestDisplayedGem(navDirection, focusedGem);
               
                // Did we find a Gem in the right direction?
                if (nextGem != null) {

                    // Decide how to handle the change of focus.
                    if (evt.isShiftDown()) {
                        // If the selection anchor is null then we are just starting to select
                        // some range of Gems.
                        if (shiftSelectionAnchorGem == null) {
                            shiftSelectionAnchorGem = nextGem;
                        }
                       
                        Rectangle2D rect = getRectangleForDisplayedGems(shiftSelectionAnchorGem, nextGem);
                        tableTop.selectGems(rect, TableTopPanel.SelectMode.REPLACE_SELECT);
                       
                    } else if (!evt.isControlDown()) {
                        // No shift key and no CTRL key so singleton select the next Gem
                        // and update the selection anchor.
                        tableTop.selectDisplayedGem(nextGem, true);
                        shiftSelectionAnchorGem = nextGem;
                    }
                   
                    // Shift the focus
                    scrollRectToVisible(nextGem.getBounds());
                    tableTop.setFocusedDisplayedGem(nextGem);
                }
               
                // Consume the key event so that the scroll bars don't get the arrow key event
                evt.consume();
            }
        }
    }


    /**
     * Navigation direction enum pattern.
     * Creation date: (04/09/2002 12:19:00 PM).
     * @author Steve Norton
     */
    static final class NavigationDirection {
       
        final String direction;
       
        static final NavigationDirection UP = new NavigationDirection("Up");
        static final NavigationDirection DOWN = new NavigationDirection("Down");
        static final NavigationDirection LEFT = new NavigationDirection("Left");
        static final NavigationDirection RIGHT = new NavigationDirection("Right");
       
        /**
         * Constructor for a navigation direction.
         */
        private NavigationDirection(String direction) {

            this.direction = direction;
        }
    }


    /**
     * Mouse handler for when the gem cutter is in run mode.
     * Creation date: Oct 09th 2002
     * @author Ken Wong
     */
    private class RunModeMouseHandler extends MouseAdapter {
       
        public void mousePressed(MouseEvent e) {
            maybeShowPopup(e);
        }
       
        public void mouseReleased(MouseEvent e) {
            maybeShowPopup(e);
        }
    }


    /**
     * Component listener for the TableTop
     * Creation date: (12/14/01 10:28:43 AM)
     * @author Edward Lam
     */
    class TableTopComponentListener extends ComponentAdapter {
        /**
         * Invoked when the component's size changes.
         */
        public void componentResized(ComponentEvent e) {
            // If the TargetGem is docked, move it to the top-right corner of the component
            tableTop.checkTargetDockLocation();
           
            // Repaint the overview.
            gemCutter.getOverviewPanel().repaint();
        }
    }


    /**
     * Handler for drag and drop events.
     * Creation date: (03/15/2002 2:03:35 PM)
     * @author Edward Lam
     */
    class TableTopDragAndDropHandler implements DropTargetListener {

        /** The image that shows up when dragging. 
         * Null if not dragging-and-dropping or if there is no drag image set. */
        private BufferedImage dragImage = null;
       
        /** The offset of the mouse from the image origin while dragging. */
        private Point mousePointOffset = new Point(0, 0);
       
        /** The bounds of the last drawn drag ghost. */
        private final Rectangle lastGhostRect = new Rectangle();
       
        /**
         * Constructor for a drag-and-drop handler
         */
        TableTopDragAndDropHandler() {
        }

        /*
         * Methods implementing DropTargetListener ************************************************************
         */
       
        /**
         * Called when a drag operation has encountered the <code>DropTarget</code>.
         * <P>
         * @param dtde the <code>DropTargetDragEvent</code>
         */
        public void dragEnter(DropTargetDragEvent dtde) {
            DataFlavor SCDF = GemEntitySelection.getEntityListDF();
            if (dtde.isDataFlavorSupported(SCDF)) {
                dtde.acceptDrag(dtde.getDropAction());

                // Set the image to drag and its offset
                dragImage = gemCutter.getBrowserTree().getDragImage();
                mousePointOffset = gemCutter.getBrowserTree().getDragOffset();

            } else {
                dtde.rejectDrag();
            }
           
            // HACK: Make up a dummy mouse event and let the ToolTipManager know that the mouse has entered
            // the table top.  We need to do this because a normal mouse entered event does not get fired when we
            // enter via DnD.  This seems to have the unfortunate effect of confusing the tool tip manager so that
            // it doesn't display tool tips when it should.  Notifying the manager when the mouse enters
            // via DnD seems to avoid the confusion.
           
            MouseEvent mouseEvt = new MouseEvent(TableTopPanel.this, MouseEvent.MOUSE_ENTERED, 0, 0, dtde.getLocation().x, dtde.getLocation().y, 1, false);
            ToolTipManager.sharedInstance().mouseEntered(mouseEvt);
           
        }
       
        /**
         * The drag operation has departed the <code>DropTarget</code> without dropping.
         * <P>
         * @param dte the <code>DropTargetEvent</code>
         */
        public void dragExit(DropTargetEvent dte) {
            // If necessary, erase the last ghost image
            if (!TableTopPanel.DRAG_IMAGE_SUPPORTED) {
                paintImmediately(lastGhostRect.getBounds());
            }
        }
       
        /**
         * Called when a drag operation is ongoing on the <code>DropTarget</code>.
         * <P>
         * @param dtde the <code>DropTargetDragEvent</code>
         */
        public void dragOver(DropTargetDragEvent dtde){
            if (!dtde.isDataFlavorSupported(GemEntitySelection.getEntityListDF())) {
                dtde.rejectDrag();
                return;
            }
            dtde.acceptDrag(dtde.getDropAction());
           
            // draw a ghost image
            // To draw the drag image:
            //   First, repaint the real estate the drag image last occupied.
            //    Note that simply calling repaint() won't work because it effectively delays the repainting,
            //    possibly until after you have drawn the new drag image, and therefore, erases all or part of it.
            //    You really must paint the area immediately, using the, you guessed it, paintImmediately() method.
            //
            //   Second, you draw the ghost image in its new location.
            //    Note that you draw the image the same distance away from the mouse pointer as when the node
            //    was first clicked.
           
            if (!TableTopPanel.DRAG_IMAGE_SUPPORTED && dragImage != null) {
                Graphics2D g2d = (Graphics2D)getGraphics();

                // Calculate new location
                Point mouseLocation = dtde.getLocation();
                int newX = mouseLocation.x - mousePointOffset.x;
                int newY = mouseLocation.y - mousePointOffset.y;
               
                // Update if the location changed
                if (newX != lastGhostRect.x || newY != lastGhostRect.y) {
                    // Erase the last ghost image and cue line
                    paintImmediately(lastGhostRect.getBounds());   
                   
                    // Remember where you are about to draw the new ghost image
                    lastGhostRect.setBounds(newX, newY, dragImage.getWidth(), dragImage.getHeight());
                   
                    // Draw the ghost image
                    g2d.drawImage(dragImage,
                            AffineTransform.getTranslateInstance(lastGhostRect.getX(), lastGhostRect.getY()),
                            null);
                }
               
                g2d.dispose();
            }
           
        }
       
        /**
         * The drag operation has terminated with a drop on this <code>DropTarget</code>.
         * This method is responsible for undertaking the transfer of the data associated with the
         * gesture. The <code>DropTargetDropEvent</code> provides a means to obtain a <code>Transferable</code>
         * object that represents the data object(s) to be transfered.<P>
         * From this method, the <code>DropTargetListener</code> shall accept or reject the drop via the  
         * acceptDrop(int dropAction) or rejectDrop() methods of the <code>DropTargetDropEvent</code> parameter.
         * <P>
         * Subsequent to acceptDrop(), but not before, <code>DropTargetDropEvent</code>'s getTransferable()
         * method may be invoked, and data transfer may be performed via the returned <code>Transferable</code>'s
         * getTransferData() method.
         * <P>
         * At the completion of a drop, an implementation of this method is required to signal the success/failure
         * of the drop by passing an appropriate <code>boolean</code> to the <code>DropTargetDropEvent</code>'s
         * dropComplete(boolean success) method.
         * <P>
         * Note: The actual processing of the data transfer is not required to finish before this method returns.
         * It may be deferred until later.
         * <P>
         * @param dtde the <code>DropTargetDropEvent</code>
         */
        public void drop(DropTargetDropEvent dtde) {
            try {
                // Get the transferable object
                Transferable trans = dtde.getTransferable();
               
                // We currently only accept our special DataFlavor
                DataFlavor SCDF = GemEntitySelection.getEntityListDF();
                if (trans.isDataFlavorSupported(SCDF)) {
                    dtde.acceptDrop(dtde.getDropAction());
                   
                    // Create Gem objects for items dropped in from the gem browser
                    List<Object> scs = UnsafeCast.<List<Object>>unsafeCast(trans.getTransferData(SCDF));

                    // Get the drop location
                    Point dropXY = dtde.getLocation();

                    int scsLen = scs.size();
                    if (scsLen < 1) {
                        return;
                    }

                    // Increment the update level for the edit undo.  This will aggregate the drops.
                    tableTop.getUndoableEditSupport().beginUpdate();

                    // Now create the gems.  Start out with the first one
                    GemEntity gemEntity = (GemEntity)scs.get(0);
                    DisplayedGem dGem = tableTop.createDisplayedFunctionalAgentGem(dropXY, gemEntity);
                   
                    // Adjust so that the the middle of the first gem appears under the pointer.
                    Rectangle dGemBounds = dGem.getBounds();
                    int halfGemWidth = dGemBounds.width / 2;
                    int halfGemHeight = dGemBounds.height / 2;
                    dropXY.translate(-halfGemWidth, -halfGemHeight);

                    // Now add the gem.
                    dGem.setLocation(dropXY);
                    tableTop.doAddGemUserAction(dGem, dropXY);

                    // Now all the other gems (if any).
                    for (int i = 1; i < scsLen; i++) {
                        gemEntity = (GemEntity)scs.get(i);               

                        // Move the dropXY a bit - the previous displayed gem's height plus some constant factor.
                        dropXY.translate(0, dGem.getBounds().height + DisplayConstants.MULTI_DROP_OFFSET);

                        // Create a new gem
                        dGem = tableTop.createDisplayedFunctionalAgentGem(dropXY, gemEntity);

                        // Now add the gem.
                        tableTop.doAddGemUserAction(dGem, dropXY);
                    }

                    // make sure the table top is large enough to hold any dropped gems
                    tableTop.resizeForGems();
                   
                    // Override the default undo name if more than one gem dropped.
                    if (scsLen > 1) {
                        tableTop.getUndoableEditSupport().setEditName(GemCutter.getResourceString("UndoText_AddGems"));
                    }

                    // Decrement the update level.  This will post the edit if the level is zero.
                    tableTop.getUndoableEditSupport().endUpdate();

                } else {
                    // We don't understand this
                    dtde.rejectDrop();
                }

            } catch (UnsupportedFlavorException ufe) {
                // Bad data flavour in drop - should never happen!
                String msgText = GemCutter.getResourceString("BadDropDataFlavour") + "\n" + ufe;
                JOptionPane.showMessageDialog(TableTopPanel.this, msgText, GemCutter.getResourceString("WindowTitle"), JOptionPane.ERROR_MESSAGE);
                dtde.rejectDrop();

            } catch (java.io.IOException ioe) {
                // Other dodginess has occurred
                String msgText = GemCutter.getResourceString("BadDropIO") + "\n" + ioe;
                JOptionPane.showMessageDialog(TableTopPanel.this, msgText, GemCutter.getResourceString("WindowTitle"), JOptionPane.ERROR_MESSAGE);
                dtde.rejectDrop();

            } finally {

                // Say that we're done with the drag and dropping thingy
                dtde.getDropTargetContext().dropComplete(true);

                // reset drag image info
                // so we don't accidentally reuse old info if one source supplies an image and another does not.
                dragImage = null;
                mousePointOffset.move(0,0);

            }  
        }
       
        /**
         * Called if the user has modified the current drop gesture.
         * <P>
         * @param dtde the <code>DropTargetDragEvent</code>
         */
        public void dropActionChanged(DropTargetDragEvent dtde) {
            DataFlavor SCDF = GemEntitySelection.getEntityListDF();
            if (dtde.isDataFlavorSupported(SCDF)) {
                dtde.acceptDrag(dtde.getDropAction());
            } else {
                dtde.rejectDrag();
            }
        }
       
    }


    /**
     * Drag action enum pattern.
     * Creation date: (31/08/2001 10:58:43 AM)
     * @author Edward Lam
     */
    private static final class TableTopDragMode extends MouseClickDragListener.DragMode {
        /*
         * GEMDRAGGING      - dragging gems around the tabletop.
         * CTRLDRAGGING     - 'cloning' function
         * CONNECTING       - connecting a gem
         * DISCONNECTING    - disconnecting a gem.  Connecting to another gem is allowed.
         * SELECTING        - selecting gems
         * USELESS          - dragging but with no effect
         */
        private static final DragMode GEMDRAGGING       = new TableTopDragMode ("Gem Dragging");
        private static final DragMode CTRLDRAGGING      = new TableTopDragMode ("Ctrl - Gem Dragging")
        private static final DragMode CONNECTING        = new TableTopDragMode ("Connecting")
        private static final DragMode DISCONNECTING     = new TableTopDragMode ("Disconnecting");
        private static final DragMode SELECTING         = new TableTopDragMode ("Selecting");  
        private static final DragMode USELESS           = new TableTopDragMode ("Useless");    

        /**
         * Constructor for a drag mode.
         */
        private TableTopDragMode(String name) {
            super(name);
        }
    }




   
    /**
     * Event listener for the TableTop
     * Creation date: (12/04/01 3:21:43 PM)
     * @author Edward Lam
     */
    class TableTopMouseHandler extends MouseClickDragListener {

        /**
         * Class to hold info for redrawing
         * Creation date: (12/17/01 12:18:43 PM)
         * @author Edward Lam
         */
        private class RedrawInfo {
            final Point pressedAt;
            final Point dragPos;
            final DragMode dragMode;
            final DisplayedGem[] dragList;
            final SelectMode selectMode;
           
            /**
             * Constructor
             */
            RedrawInfo(Point pressedAt, Point dragPos, DragMode dragMode,
                    DisplayedGem[] dragList, SelectMode selectMode){
                this.pressedAt = pressedAt;
                this.dragPos = dragPos;
                this.dragMode = dragMode;
                this.dragList = dragList;
                this.selectMode = selectMode;
            }
        }

        //
        // Mouse states and state associated with it -------------------------------------------------------
        //
       
        /** The gem that we pressed on and are dragging. */
        private DisplayedGem clickGem;

        /** The displayed input or output where the connection drag originated. */
        private DisplayedPartConnectable connectionDragAnchorPart;

        /** If disconnecting, the part that was disconnected. */
        private DisplayedPartConnectable disconnectedDisplayedPart = null;

        /** What the last select mode was. */       
        private SelectMode lastSelectMode;

        /** List of Gems being dragged. */
        private DisplayedGem[] dragList;

        /** The last position for dragging. */
        private Point dragPos;

        /** The clip area present for the last drag. */
        private Shape lastDragClipArea;
       
        /** The colour present for the last drag. */
        private Color lastDragColour;
       
        /** Information pertaining to the most recent draw (if any). */
        private RedrawInfo redrawInfo = null;
       
        /** Whether a drag operation started by pressing & dragging over a VEP. */
        private boolean dragStartedOverVEP = false;

        // Maybe uncomment when disconnection happens properly.
//        /** (PartInput->AutoburnLogic.BurnStatus) If CONNECTING or DISCONNECTING, map from inputs which have been automatically burnt or unburnt,
//         *  but have not have had edits committed (since the action may be canceled) to their burn state before the action was initiated */
//        private Map transientInputToOldBurnStateMap = new HashMap();
//
        /**
         * Constructor for a TableTopMouseHandler
         */
        TableTopMouseHandler() {
            super();
        }
       
        /**
         * Returns the drag mode
         * @return DragMode
         */
        DragMode getDragMode() {
            return dragMode;
        }

        /**
         * Move the drag mode into the aborted state.
         */
        protected void abortDrag() {

            try {
                // Finish the ghost drawing (undraw the last lot)
                Graphics2D g2d = (Graphics2D)getGraphics();  
                drawDragGhost(DrawAction.UNDRAW, lastSelectMode, g2d);
                g2d.dispose();
               
                // if we're connecting, undo automatically burned inputs on the source
                if (dragMode == TableTopDragMode.CONNECTING || dragMode == TableTopDragMode.DISCONNECTING) {
                    DisplayedGem burnGem = connectionDragAnchorPart.getDisplayedGem();
                    tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(burnGem.getGem());
                   
                    // Decrement the update level.  This will post the edit if the level is zero.
                    tableTop.getUndoableEditSupport().endUpdate();
                }

            } finally {

                // Set our drag states appropriately
                super.abortDrag();
                tableTop.selectDisplayedGem(null, false);
                tableTop.setFocusedDisplayedGem(null);
                setCursor(null);
            }
        }

        /**
         * Assuming that we are in the TableTopDragMode.CONNECTING state, change the tabletop state to take into account
         * the current drag position.  This changes the cursor, and may attempt to carry out or undo autoburn
         * on the connection source.
         * @param where Point The mouse location to check.
         */
        private void changeStateForConnecting(Point where) {
           
            // Check if we are over a part that is not yet bound to another part.
            DisplayedGem.DisplayedPart partUnder = tableTop.getGemPartUnder(where);

            // Make sure we are trying to connect inputs to outputs and vice versa.
            if ((connectionDragAnchorPart instanceof DisplayedPartOutput && partUnder instanceof DisplayedPartInput) ||
                    (connectionDragAnchorPart instanceof DisplayedPartInput && partUnder instanceof DisplayedPartOutput)){

                // Assign the source and sink parts depending what we are hovering over.
                PartConnectable sourcePart;
                PartInput sinkPart;
               
                if (connectionDragAnchorPart instanceof DisplayedPartOutput) {
                    sourcePart = connectionDragAnchorPart.getPartConnectable();
                    sinkPart = ((DisplayedPartInput)partUnder).getPartInput();
                   
                } else {
                    sourcePart = ((DisplayedPartConnectable)partUnder).getPartConnectable();
                    sinkPart = ((DisplayedPartInput)connectionDragAnchorPart).getPartInput();
                }
               
                Gem sourceGem = sourcePart.getGem();
                Gem sinkGem = sinkPart.getGem();
               
                if (sourceGem instanceof ValueGem) {
                   
                    // Check if a value gem can be connected.
                    // They get special treatment since autoburning doesn't apply to value gems.
                   
                    ModuleTypeInfo currentModuleTypeInfo = tableTop.getCurrentModuleTypeInfo();
                    ValueEditorManager valueEditorManager = gemCutter.getValueEditorManager();
                   
                    if (!valueEditorManager.canInputDefaultValue(sinkPart.getType())) {
                        setCursor(connectNoParkCursor);
                       
                    } else if (GemGraph.isCompositionConnectionValid(sourcePart, sinkPart, currentModuleTypeInfo)) {
                        setCursor(connectCursor);
                       
                    } else if (GemGraph.isDefaultableValueGemSource(sourcePart, sinkPart, gemCutter.getConnectionContext())) {
                        setCursor(connectCursor);
                       
                    } else {
                        setCursor(connectNoParkCursor);
                    }
                } else if (sinkGem instanceof RecordFieldSelectionGem && !((RecordFieldSelectionGem)sinkGem).isFieldFixed()) {
                    if (GemGraph.isValidConnectionToRecordFieldSelection(sourcePart, sinkPart, tableTop.getCurrentModuleTypeInfo()) != null) {
                        setCursor(connectCursor);
                    } else {
                        setCursor(connectNoParkCursor);
                    }
                } else if (GemGraph.arePartsConnectable(sourcePart, sinkPart)) {

                    // Check if the parts can be connected either through burning or direct connection.
                    // Even if they can be connected without autoburning we want to at least try burning them.
                    // Why? Because it might be better to burn the gem than just connecting it. We want to
                    // at least consider that possibility and recommend the best choice to the user.

                    if (tableTop.getBurnManager().getAutoburnLastResult() == AutoburnLogic.AutoburnAction.BURNED) {

                        // If the gem has already been burnt previously then keep showing the burn cursor.
                        // Don't try and burn the gem again since that will just show the connect cursor,
                        // since the autoburn logic already sees the gem as being burnt.

                        setCursor(burnCursor);
                       
                    } else if (GemGraph.isConnectionValid(sourcePart, sinkPart)) {
                       
                        // Try making a connection using autoburning.
                       
                        AutoburnLogic.AutoburnAction autoBurnResult = tableTop.getBurnManager().handleAutoburnGemGesture(sourceGem, sinkPart.getType(), true);
                       
                        if (autoBurnResult == AutoburnLogic.AutoburnAction.BURNED) {
                            setCursor(burnCursor);
                           
                        } else if (autoBurnResult == AutoburnLogic.AutoburnAction.MULTIPLE) {
                            setCursor(burnQuestionCursor);
                           
                        } else if (autoBurnResult == AutoburnLogic.AutoburnAction.IMPOSSIBLE) {
                            setCursor(connectNoParkCursor);
                           
                        } else {
                            setCursor(connectCursor);
                        }

                    } else {
                       
                        // Won't connect through autoburning.
                        setCursor(connectNoParkCursor);
                    }

                } else {

                    // The connection as a whole is invalid (eg. destination already connected).
                    setCursor(connectNoParkCursor);
                }

            } else {
               
                // We're not trying to connect a sink. Reset the cursor.
                setCursor(null);

                // If we aren't disconnecting, we should undo any autoburn we previously performed (if any).
                if (dragMode != TableTopDragMode.DISCONNECTING && tableTop.getBurnManager().getAutoburnLastResult() == AutoburnLogic.AutoburnAction.BURNED) {
                    tableTop.getBurnManager().handleAutoburnGemGesture(connectionDragAnchorPart.getGem(), null, false);
                }
            }
        }

        /**
         * Check whether the tabletop needs expanding, and take care of the expansion grunt work if necessary.
         * Note that the "where" parameter may be modified during the execution of this method, to take
         * into account the new coordinates in an expanded tabletop.
         * @param where Point The mouse location to check.
         * @return boolean Whether the tabletop expanded.
         */
        private boolean checkExpand(Point where) {
           
            Rectangle visibleRect = getVisibleRect();

            // if the point is in the visible bounds of the tabletop, we're ok.
            if (visibleRect.contains(where)) {
                return false;
            }
           
            // otherwise, we may need to expand. Declare the expand flag.
            boolean expand = false;
           
            // the dimension of the new tabletop if we expand
            Dimension dim = new Dimension();
           
            int whereX = where.x;
            int whereY = where.y;

            //
            // x-axis expansion
            //
            if (whereX < 0) {
               
                // expand left
                expand = true;
                int moveDistance = -(whereX);

                // Translate the gemgraph and tabletop points to the right
                tableTop.moveAllGems(moveDistance, 0);
                pressedAt.x += moveDistance;
                where.x += moveDistance;
                backgroundImageOriginOffsetX += moveDistance;
               
                // update the last clip bounds so that the old drag ghosts will be undrawn properly
                if (lastDragClipArea != null) {
                    Rectangle rect = lastDragClipArea.getBounds();
                    rect.x += moveDistance;
                    lastDragClipArea = rect;
                }

                dim = new Dimension(getWidth() + moveDistance, getHeight());

            } else if (whereX > getSize().width) {

                // Expand right.
                expand = true;
                int expandDistanceX = whereX - getSize().width;
                dim = new Dimension(getWidth() + expandDistanceX, getHeight());
            }  
           
            //
            // y-axis expansion
            //
            if (whereY < 0) {

                // Expand up       
                expand = true;                 
                int moveDistance = -(whereY);

                // Translate the gemgraph and tabletop points down
                tableTop.moveAllGems(0, moveDistance);
                pressedAt.y += moveDistance;
                where.y += moveDistance;
                backgroundImageOriginOffsetY += moveDistance;
               
                // update the last clip bounds so that the old drag ghosts will be undrawn properly
                if (lastDragClipArea != null) {
                    Rectangle rect = lastDragClipArea.getBounds();
                    rect.y += moveDistance;
                    lastDragClipArea = rect;
                }

                dim = new Dimension(getWidth(), getHeight() + moveDistance);
               
            } else if (whereY > getSize().height) {

                // Expand down.
                expand = true;
               
                int expandDistanceY = whereY - getSize().height + 1;    // + 1 so we can see the last pixel
                dim = new Dimension(getWidth(), getHeight() + expandDistanceY);
            }

           
            // now take appropriate action now that we decided whether or not to expand
            if (expand) {
                // Set the size of the tabletop.  This also invokes revalidation since it's a JContainer.
                setSize(dim);          
                setPreferredSize(dim);
                revalidate();
            }
           
            return expand;
        }
       
        /**
         * Draw the appropriate drag ghost according to the current drag mode
         *   - the drag ghost may be a connection line, gem drag ghost, or drag selection outline.
         *
         * @param drawAction the current draw action. If this is DrawAction.REDRAW, then dragAction and dragSelectMode
         * do not have to be provided.
         * @param selectMode the mode to draw the drag ghost in. This can be one of three modes:
         *  <br>REPLACE_SELECT:     Solid line - select only enclosed Gems
         *  <br>TOGGLE:             Wavy line - toggle selection of enclosed Gems
         *  <br>SELECT:             Dotted line - select enclosed Gems, preserving ones already selected
         *  <br>This parameter can be ignored if the drag action is not drag selecting.
         * @param graphics Graphics2D the graphics context to use.
         *
         */
        synchronized void drawDragGhost(DrawAction drawAction, SelectMode selectMode, Graphics2D graphics) {
           
            Point pressedAt, dragPos;
            DragMode dragMode;
            DisplayedGem[] dragList;

            // set draw info based on whether or not we are redrawing.
            if (drawAction == DrawAction.REDRAW) {
                if (redrawInfo == null) {
                    return;         // nothing to redraw
                }

                // reload the draw info
                pressedAt = redrawInfo.pressedAt;
                dragPos = redrawInfo.dragPos;
                dragMode = redrawInfo.dragMode;
                dragList = redrawInfo.dragList;
                selectMode = redrawInfo.selectMode;

            } else {
                try {
                    // there's a chance that click and drag positions could be modified from the event dispatch thread
                    // and that this method is called from the painting thread.  Thus, click and drag positions could be
                    // concurrently used and modified in the middle of this thread!  Thus we clone the points locally:
                    pressedAt = (Point)this.pressedAt.clone();
                    dragPos   = (Point)this.dragPos.clone();
                    dragMode = this.dragMode;
                    dragList = this.dragList;
                } catch (NullPointerException npe) {
                    // nothing to (un)draw..
                    return;
                }
            }
           
            // check for nothing to do
            if (lastDragClipArea == null && drawAction == DrawAction.UNDRAW) {
                redrawInfo = null;
                return;
            }
           
            // Get the current graphics context
            Graphics2D g2d = (Graphics2D)getGraphics();
           
            // clip further, and update our clip areas, if necessary
            if (drawAction == DrawAction.DRAW || drawAction == DrawAction.REDRAW) {
               
                Shape graphicsClip = graphics.getClip();
               
                if (graphicsClip != null) {
                    // clip to the clip area derived from the passed in graphics object
                    g2d.setClip(graphicsClip);
                } else {
                    // make sure there is a clip, so that the check for null above doesn't fail inappropriately on undraw
                    g2d.setClip(getVisibleRect());
                }
               
                // update the latest clip area
                lastDragClipArea = g2d.getClip();
               
                // update redraw info
                redrawInfo = new RedrawInfo(pressedAt, dragPos, dragMode, dragList, selectMode);
               
            } else if (drawAction == DrawAction.UNDRAW) {
               
                // use the clip area present when the ghost was last drawn
                g2d.setClip(lastDragClipArea);
               
                // update the latest clip area
                lastDragClipArea = null;
               
                // clear redraw info if we're undrawing, since there will be nothing to redraw
                redrawInfo = null;
               
            }
           
            // Enter XOR mode
            g2d.setXORMode(Color.white);
           
            // Now (un)draw the drag ghost according to the drag action indicated
            if (dragMode == TableTopDragMode.GEMDRAGGING || dragMode == TableTopDragMode.CTRLDRAGGING) {
               
                // For each gem, draw a ghost
                for (final DisplayedGem dGem : dragList) {
                    // The ghost is the gem body translated by the drag offset
                    Shape ghost = dGem.getDisplayedGemShape().getBodyShape();
                   
                    // Translate into the correct location (how we do this depends on the type)
                    int translateX = dragPos.x - pressedAt.x;
                    int translateY = dragPos.y - pressedAt.y;
                    if (ghost instanceof Polygon) {
                        ((Polygon)ghost).translate(translateX, translateY);
                    } else if (ghost instanceof RectangularShape) {
                        RectangularShape rectGhost = (RectangularShape)ghost;
                        Rectangle bounds = rectGhost.getBounds();
                        rectGhost.setFrame(bounds.x + translateX, bounds.y + translateY,
                                bounds.getWidth(), bounds.getHeight());
                    }
                   
                    g2d.setStroke(new BasicStroke((float) 2.0));   
                    g2d.draw(ghost);
                }
               
            } else if (dragMode == TableTopDragMode.CONNECTING || dragMode == TableTopDragMode.DISCONNECTING) {
               
                // Figure out which point is the beginning point of the connection based on the type of
                // the part where the connection originated.
                Point fromPoint, toPoint;
                if (tableTop.getGemPartUnder(pressedAt) instanceof DisplayedPartOutput) {
                    fromPoint = pressedAt;
                    toPoint = dragPos;
                   
                } else {
                    fromPoint = dragPos;
                    toPoint = pressedAt;
                }

                ConnectionRoute route = new ConnectionRoute(fromPoint, toPoint);
                DisplayedConnection.genConnectionRoute(route, DisplayConstants.REVERSE_CONNECTION_HOOK_SIZE);
               
                // set, save the draw color
                if (drawAction == DrawAction.DRAW || drawAction == DrawAction.REDRAW) {
                    // Set colour to the appropriate colour for the output type implied.
                    Color connectColour = tableTop.getTypeColour(connectionDragAnchorPart);
                    g2d.setColor(connectColour);

                    // save the last drag colour so we can undraw later in the same colour
                    lastDragColour = connectColour;

                } else {
                    // undraw in the last drag colour
                    g2d.setColor(lastDragColour);
                }
               
                // Draw a connection line
                route.draw(g2d);
               
            } else if (dragMode == TableTopDragMode.SELECTING) {
               
                // Set stroke characteristics.  Depends on mode
                if (selectMode == SelectMode.TOGGLE) {
                    g2d.setStroke(new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                    //g2d.setStroke(new com.sun.glf.goodies.WaveStroke(1, 5, 2));
                   
                } else if (selectMode == SelectMode.SELECT) {
                    g2d.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 8f, new float[]{6f, 6f}, 0f));
                   
                } else {
                    // the default case: DragSelectMode.REPLACE_SELECT:
                    g2d.setStroke(new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
                    //g2d.setStroke(new com.sun.glf.goodies.TextStroke("Select", new Font("dialog", Font.PLAIN, 12), true, 0));
                }
               
                // It turns out drawing four lines is significantly faster than calling drawRect(int, int, int, int)
                // This is most noticeable in xor mode (which we use for this drag ghost)
                g2d.drawLine(pressedAt.x, pressedAt.y, pressedAt.x, dragPos.y);
                g2d.drawLine(pressedAt.x, pressedAt.y, dragPos.x, pressedAt.y );
                g2d.drawLine(pressedAt.x, dragPos.y, dragPos.x, dragPos.y);
                g2d.drawLine(dragPos.x, pressedAt.y, dragPos.x, dragPos.y);
               
            }
           
            // dispose the graphics object
            g2d.dispose();
        }
       
        /**
         * Carry out setup appropriate to enter the drag state.  Principal effect is to change dragMode as appropriate.
         * @param e MouseEvent the mouse event which triggered entry into the drag state.
         */
        public void enterDragState(MouseEvent e) {

            // Store the focus state and move the focus to the table top for now
            tableTop.saveFocus();
            requestFocus();
           
            // popups should not be shown in most drag states
            popupShouldShow = false;
           
            // find the drag mode appropriate to the place on which we initiated the drag
            DragMode nextMode = getDragModeForDragOrigin();
            ExtendedUndoableEditSupport undoableEditSupport = tableTop.getUndoableEditSupport();

            if (nextMode == TableTopDragMode.GEMDRAGGING && SwingUtilities.isLeftMouseButton(e)) {

                if (e.isControlDown()) {
                    // Ensure that the gem beneath the cursor is selected when we begin the drag.
                    DisplayedGem clickedGem = tableTop.getGemUnder(pressedAt);
                    tableTop.selectDisplayedGem(clickedGem, false);
                    tableTop.setFocusedDisplayedGem(clickedGem);
                    shiftSelectionAnchorGem = clickedGem;
                   
                    repaint();
                    nextMode = TableTopDragMode.CTRLDRAGGING;
                }

                // dragging gems around
                setDragMode(nextMode);
               
                // Create the dragList of selected Gems
                dragList = tableTop.getSelectedDisplayedGems();
               
            } else if (nextMode == TableTopDragMode.CONNECTING) {

                // connecting up gems
                dragMode = nextMode;

                // Increment the update level to aggregate any burns with connection change edits.
                undoableEditSupport.beginUpdate();
               
            } else if (nextMode == TableTopDragMode.DISCONNECTING) {
               
                // disconnecting gems
                dragMode = nextMode;

                // Increment the update level to aggregate any unburns with connection change edits.
                undoableEditSupport.beginUpdate();
               
                // Get the part which the user pressed.  Should be connectable!
                DisplayedPartConnectable partPressed =
                    (DisplayedPartConnectable)tableTop.getGemPartUnder(pressedAt);

                // Keep track of the disconnected part.
                disconnectedDisplayedPart = partPressed;

                // Get the gem connection to disconnect.
                DisplayedConnection disconnectConn = partPressed.getDisplayedConnection();
               
                // Adjust the apparent click point to be the point of the arrow of the part that is
                // not being disconnected.
                if (partPressed instanceof DisplayedPartOutput) {
                    connectionDragAnchorPart = disconnectConn.getDestination();
                } else {
                    connectionDragAnchorPart = disconnectConn.getSource();
                }
               
                pressedAt = connectionDragAnchorPart.getConnectionPoint();                     
               
                // Indicate to the user that a drag disconnect is possible.
                setCursor(connectCursor);            

                // Disconnect the connection.
                tableTop.handleDisconnectGesture(disconnectConn.getConnection());

                // Undo any autoburns if we disconnected an output
                if (partPressed instanceof DisplayedPartOutput) {
                    DisplayedGem burnGem = partPressed.getDisplayedGem();
                    tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(burnGem.getGem());
                }

            } else if (nextMode == TableTopDragMode.SELECTING && SwingUtilities.isLeftMouseButton(e)) {
               
                // drag selecting
                dragMode = nextMode;
               
            } else {
               
                // not dragging really, so we can display popups
                dragMode = TableTopDragMode.USELESS;
                popupShouldShow = true;
            }
           

        }      

        /**
         * Carry out setup appropriate to exit the drag state. 
         * Undraw the drag ghost and carry out translating/connecting/selecting
         * @param e MouseEvent the mouse event which caused an exit from the drag state
         */
        public void exitDragState(MouseEvent e) {

            // Finish the ghost drawing (undraw the last lot)
            if (isUsefulDragMode(dragMode)) {
                Graphics2D g2d = (Graphics2D)getGraphics();  
                drawDragGhost(DrawAction.UNDRAW, SelectMode.REPLACE_SELECT, g2d);
                g2d.dispose();
            }
           
            // Where are we now? 
            // We use dragPos instead of e.getPoint() so that the gem drops where the drag ghost shows it.
            Point where = dragPos;
            ExtendedUndoableEditSupport undoableEditSupport = tableTop.getUndoableEditSupport();

            // Now do whatever based on the drag mode
            if (dragMode == TableTopDragMode.CTRLDRAGGING) {
               
                // Ctrl Drag is used as a clone function
                // We want to group this stuff as one single action
                undoableEditSupport.beginUpdate();
                undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_CopyDrag"));
                DisplayedGemSelection displayedGemSelection = new DisplayedGemSelection(dragList, gemCutter);
                Rectangle rect = dragList[0].getBounds();
                for (int i = 1; i < dragList.length; i++) {
                    rect.add(dragList[i].getBounds());
                }
                int x = where.x - (pressedAt.x - rect.x);
                int y = where.y - (pressedAt.y - rect.y);
               
                tableTop.doPasteUserAction(displayedGemSelection, new Point(x, y));
                undoableEditSupport.endUpdate();

            } else if (dragMode == TableTopDragMode.GEMDRAGGING) {
               
                // Increment the update level for the edit undo.  This will aggregate the gem translations.
                undoableEditSupport.beginUpdate();
                if (dragList.length > 0) {
                    undoableEditSupport.setEditName(dragList.length > 1 ? GemCutter.getResourceString("UndoText_MoveGems") :
                                                                          GemCutter.getResourceString("UndoText_MoveGem"));
                }

                // Do the move for each selected gem
                for (final DisplayedGem displayedGem : dragList) {
                    Point newGemLocation = displayedGem.getLocation();
                    newGemLocation.translate(where.x - pressedAt.x, where.y - pressedAt.y);
                   
                    // Perform the translation
                    tableTop.doChangeGemLocationUserAction(displayedGem, newGemLocation);
                }

                // Decrement the update level.  This will post the edit if the level is zero.
                undoableEditSupport.endUpdate();
               
            } else if (dragMode == TableTopDragMode.CONNECTING || dragMode == TableTopDragMode.DISCONNECTING) {

                // see if we can connect anything
                DisplayedPart partUnder = tableTop.getGemPartUnder(where);

                boolean connected = false;
                if (partUnder != null) {
                   
                    Connection newConnection = null;
                    if (connectionDragAnchorPart instanceof DisplayedPartOutput && partUnder instanceof DisplayedPartInput) {
                        newConnection = tableTop.handleConnectGemPartsGesture(connectionDragAnchorPart.getPartConnectable(),
                                                                              ((DisplayedPartInput)partUnder).getPartInput());
                    } else if (partUnder instanceof DisplayedPartConnectable && connectionDragAnchorPart instanceof DisplayedPartInput) {
                        newConnection = tableTop.handleConnectGemPartsGesture(((DisplayedPartConnectable)partUnder).getPartConnectable(),
                                                                              ((DisplayedPartInput)connectionDragAnchorPart).getPartInput());
                    }
                   
                    connected = (newConnection != null);
                }

                // Undo any autoburns if we didn't connect anything
                if (!connected && connectionDragAnchorPart instanceof DisplayedPartOutput) {
                    DisplayedGem burnGem = connectionDragAnchorPart.getDisplayedGem();
                    tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(burnGem.getGem());
                }

                if (!connected && dragMode == TableTopDragMode.CONNECTING) {
                    // Don't post the edit if connecting and nothing happened.
                    undoableEditSupport.endUpdateNoPost();

                } else if (connected && dragMode == TableTopDragMode.DISCONNECTING && disconnectedDisplayedPart == partUnder){
                    // Also don't post the edit if all we did was reconnect a part that we disconnected.
                    undoableEditSupport.endUpdateNoPost();

                } else {
                   
                    if (dragMode == TableTopDragMode.CONNECTING) {
                        undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_ConnectGems"));

                    } else {
                        undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_DisconnectGems"));
                    }

                    // Decrement the update level, possibly triggering the edit to be posted.
                    undoableEditSupport.endUpdate();
                }

            } else if (dragMode == TableTopDragMode.SELECTING && pressedAt != null) {

                // calculate the bounds of the select area     
                Rectangle hitRect = new Rectangle(pressedAt);
                hitRect.add(where);

                // Perform appropriate selection operation for each intersecting gem.
                // We need to use the TOGGLE selection mode if the drag was started with the
                // CTRL modifier or the SELECT selection mode if the drag was started with the
                // SHIFT modifier... otherwise just use the last mode.  The SHIFT modifier seems to
                // mask the CTRL modifier in this instance so look for that first here.
                SelectMode selMode;
                if (dragInitiatedWithSHIFT) {
                    selMode = SelectMode.SELECT;
                   
                } else if (dragInitiatedWithCTRL) {
                    selMode = SelectMode.TOGGLE;
                   
                } else {
                    selMode = lastSelectMode;
                }
               
                tableTop.selectGems(hitRect, selMode);
               
                // todoSN - the Windows Desktop does not alter the focused icon when drag selection is
                // performed so we won't here either.  Unfortunately, this may result in no gems having
                // focus if a gem is not specifically clicked so we will give focus on a drag if the
                // existing focused gem is null.  This is probably just a temporary fix for this issue.
                // If the existing focused gem is null give focus to the Gem that is closest to the point
                // where dragging started and inside the drag rectangle.
                DisplayedGem[] selectedGems = tableTop.getSelectedDisplayedGems();
                double dist = -1;
                DisplayedGem gemToFocusOn = null;

                for (final DisplayedGem dGem : selectedGems) {

                    Point2D centrePoint = dGem.getCenterPoint();
                    if (hitRect.contains(centrePoint)) {
                        double thisDist = pressedAt.distance(centrePoint);

                        if (dist < 0 || thisDist < dist) {
                            dist = thisDist;
                            gemToFocusOn = dGem;
                        }
                    }
                }

                // Actually set the focus here if we found a gem to give focus to.  If the user is
                // replace selecting then we need to update the focus no matter what.
                if (tableTop.getFocusedDisplayedGem() == null
                        && (gemToFocusOn != null || lastSelectMode == SelectMode.REPLACE_SELECT)) {

                    tableTop.setFocusedDisplayedGem(gemToFocusOn);
                }
               
                // Only update the selection anchor if the SHIFT and CTRL modifiers are NOT used on
                // mouse button release.
                if (!e.isShiftDown() && !e.isControlDown()) {
                    shiftSelectionAnchorGem = gemToFocusOn;
                }
            }

            // Dragging is finished.  Reset dragMode.
            dragMode = DragMode.NOTDRAGGING;
        }      

        /**
         * Get the drag mode appropriate to the origin of the current drag
         * @return DragMode the drag mode appropriate to the origin of the current drag.
         */
        private DragMode getDragModeForDragOrigin() {

            // Get the part which the user pressed 
            DisplayedPart partPressed = null;
            if (pressedAt != null) {
                partPressed = tableTop.getGemPartUnder(pressedAt);
            }

            // default is selecting (if not dragging or composing)
            DragMode returnMode = TableTopDragMode.SELECTING;

            // Did they hit anything?
            if (partPressed != null) {
               
                if (partPressed instanceof DisplayedPartConnectable &&
                        ((DisplayedPartConnectable)partPressed).getPartConnectable().isConnected()){
                    // Disconnecting
                    returnMode = TableTopDragMode.DISCONNECTING;

                } else if (partPressed instanceof DisplayedPartBody) {
                    // Dragging gem(s)
                    returnMode = TableTopDragMode.GEMDRAGGING;             
                   
                } else if (partPressed instanceof DisplayedPartConnectable) {
                    // We got an input or output.
                    returnMode = TableTopDragMode.CONNECTING;
                   
                }
            }
            return returnMode;
        }

        /**
         * Get the select mode appropriate to the modifiers on the mouse event
         * @param e MouseEvent the related mouse event
         * @return SelectMode the selection mode appropriate to the modifiers on the mouse event
         */
        private SelectMode getSelectModeForEvent(MouseEvent e) {
           
            if (e.isShiftDown()) {
                return SelectMode.SELECT;

            } else if (e.isControlDown()) {
                return SelectMode.TOGGLE;

            } else {
                return SelectMode.REPLACE_SELECT;
            }      

        }

        /**
         * Whether this drag mode actually enables accomplishing anything.
         *   Gem dragging, connecting, disconnecting, and selecting are useful.
         *   Aborted, useless, and not-dragging states are not useful.
         * @param mode DragMode the DragMode to check
         * @return boolean true if accomplishing anything with this drag. 
         */
        protected final boolean isUsefulDragMode(DragMode mode) {
            return (mode == TableTopDragMode.GEMDRAGGING ||
                    mode == TableTopDragMode.CONNECTING ||
                    mode == TableTopDragMode.DISCONNECTING ||
                    mode == TableTopDragMode.SELECTING ||
                    mode == TableTopDragMode.CTRLDRAGGING);
        }

        /**
         * If the current dragmode is either GEMDRAGGING or CTRLDRAGGING,
         * then it is considered a 'gemdragging' action.
         * @return boolean
         */
        boolean isGemDragging() {
            return ((dragMode == TableTopDragMode.GEMDRAGGING) || (dragMode == TableTopDragMode.CTRLDRAGGING));
        }

        void setDragMode(DragMode mode) {
            if (mode == TableTopDragMode.CTRLDRAGGING) {
                setCursor(cloneGemCursor);
            } else {
                setCursor(null);
            }
            dragMode = mode;
        }

        /**
         * Add a gem to the tabletop if appropriate.
         * This should be called from mousePressed()
         * @return boolean true only if a gem was added to the tabletop
         */
        private boolean maybeAddGem() {

            // If we are adding a Gem, click position indicates the position of the Gem
            if (gemCutter.getGUIState() == GemCutter.GUIState.ADD_GEM) {

                // Tell the GemCutter where to add the gem
                DisplayedGem addingGem = gemCutter.getAddingDisplayedGem();
                if (addingGem != null) {
                    tableTop.doAddGemUserAction(addingGem, pressedAt);
                   
                } else {
                    DisplayedPart part = tableTop.getGemPartUnder(pressedAt);
                    boolean showedIntellicutForPart = false;
                   
                    // Check if the user clicked on a part and if we should start Intellicut for that.
                    if (part instanceof DisplayedPartConnectable) {
                        showedIntellicutForPart = tableTop.maybeStartIntellicutMode(part);
                    }
                   
                    // Just show Intellicut for the table top if we didn't show it for a part
                    if (!showedIntellicutForPart) {
                       
                        // If there was a part use it's bounds as the display rect. That way if the user clicks on
                        // a gem body part, the list wont obscure the part.
                        Rectangle displayRect = part != null ? part.getBounds() : new Rectangle(pressedAt);
                       
                        // Use the lower-right of the display rect as the drop point. That way if the user clicks
                        // a gem body part the new gem will appear next to the old one, not over it.
                        Point dropPoint = new Point(displayRect.x + displayRect.width, displayRect.y + displayRect.height);
                       
                        gemCutter.getIntellicutManager().startIntellicutModeForTableTop(displayRect, dropPoint);
                    }
                }
               
                // Back to edit mode
                gemCutter.enterGUIState(GemCutter.GUIState.EDIT);
               
                // Disallow further action (eg. drag)
                abortDrag();
               
                // We added a gem
                return true;
            }
            return false;
        }

        /**
         * Invoked when a mouse button is pressed on a component and then
         * dragged.  Mouse drag events will continue to be delivered to
         * the component where the first originated until the mouse button is
         * released (regardless of whether the mouse position is within the
         * bounds of the component).
         */
        public void mouseDragged(MouseEvent e) {
           
            try {
                // If we're still painting, or we bailed out, do nothing.
                if (isPainting != 0 || dragMode == DragMode.ABORTED) {
                    return;
                }
               
                // If needed forward the mouse event to the vep.
                if (dragStartedOverVEP && valueEntryPanelHit(e.getPoint())) {
                    forwardMouseEvent (getValueEntryPanel((ValueGem)clickGem.getGem()), e);
                    return;
                }
               
                if (dragStartedOverVEP) {
                    return;
                }
               
                // Defer to the superclass method
                super.mouseDragged(e);

            } catch (Throwable t) {
                // some error occurred.  Treat this as an aborted drag.
                abortDrag();
                t.printStackTrace();
            }
        }

        /**
         * {@inheritDoc}
         */
        public void mouseEntered(MouseEvent e) {
        }

        /**
         * {@inheritDoc}
         */
        public void mouseExited(MouseEvent e) {
        }

        /**
         * Invoked when the mouse has been moved on a component
         * (with no buttons no down).
         */
        public void mouseMoved(MouseEvent e) {
           
            // Test if we are over a gem part
            DisplayedPart partUnder = tableTop.getGemPartUnder(e.getPoint());
           
            // If we are dragging over a VEP make sure to display the correct cursor.
            if (valueEntryPanelHit(e.getPoint())) {
                ValueEntryPanel vep = getValueEntryPanel((ValueGem)partUnder.getGem());
                Point vepPoint = SwingUtilities.convertPoint(TableTopPanel.this, e.getPoint(), vep);
                setCursor(vep.getCursor(vepPoint));
               
            } else {
                setCursor(Cursor.getDefaultCursor());
            }
           
            // display some help maybe
            if (gemCutter.getGUIState() == GemCutter.GUIState.EDIT) {

                // unconnected connectable parts of a non-broken gem
                if (partUnder instanceof DisplayedPartConnectable &&                                // connectable
                    !((DisplayedPartConnectable)partUnder).getPartConnectable().isConnected() &&    // not connected
                    !((partUnder.getGem().getRootGem() != null) &&                                  // not ancestor of a broken forest
                      GemGraph.isAncestorOfBrokenGemForest(partUnder.getGem().getRootGem()))) {
                   
                    if (partUnder instanceof DisplayedPartInput) {
                        // double click to burn/unburn an unconnected input
                        gemCutter.getStatusMessageDisplayer().setMessageFromResource(TableTopPanel.this, "SM_DblClickBurn", StatusMessageDisplayer.MessageType.PERSISTENT);
                       
                    } else {
                        gemCutter.getStatusMessageDisplayer().clearMessage(TableTopPanel.this);
                    }

                } else {
                    gemCutter.getStatusMessageDisplayer().clearMessage(TableTopPanel.this);
                }

            } else {
                // not in edit mode
                gemCutter.getStatusMessageDisplayer().clearMessage(TableTopPanel.this);
            }
        }

        /**
         * Invoked when a mouse button has been pressed on a component.
         */
        public void mousePressed(MouseEvent e){
            try {
                // move focus to the tabletop
                requestFocus();

                dragStartedOverVEP = false;
               
                // A mousePress should stop Intellicut.
                IntellicutManager.IntellicutMode prevIntellicutMode = tableTop.getIntellicutManager().getIntellicutMode();
                tableTop.getIntellicutManager().stopIntellicut();

                // Ignore clicks unless we're editing or adding gems
                GemCutter.GUIState GUIState = gemCutter.getGUIState();
                if ((GUIState != GemCutter.GUIState.EDIT) && (GUIState != GemCutter.GUIState.ADD_GEM)) {
                    return;
                }
               
                // Call the superclass method
                super.mousePressed(e);
               
                // see if this resulted in an aborted drag
                if (dragMode == DragMode.ABORTED) {
                    return;
                }

                // add a gem if appropriate
                if (maybeAddGem()) {
                    return;
                }
               
                DisplayedPart partPressed = tableTop.getGemPartUnder(pressedAt);
               
                // Now do whatever based on what was pressed on
                mousePressedOn(e, partPressed, prevIntellicutMode);
               
                // Added for Linux compatibility (KDE popups are shown on mouse down)
                if (!isUsefulDragMode(dragMode)) {
                    maybeShowPopup(e);
                }

            } catch (Throwable t) {
                // some error occurred.  Treat this as an aborted drag.
                abortDrag();
                t.printStackTrace();
            }
        }

        /**
         * Take action based on what was pressed.
         * @param e the relevant event
         * @param partPressed the part which was pressed
         * @param prevIntellicutMode the intellicut mode before the press occurred
         */
        public void mousePressedOn(MouseEvent e, DisplayedPart partPressed, IntellicutManager.IntellicutMode prevIntellicutMode) {
           
            // Did they hit anything?
            if (partPressed != null) {

                if (partPressed instanceof DisplayedPartBody) {
                    // Make sure the popup menus for running gems are closed.  Their
                    // ability to alter focus and selection of gems on the TableTop
                    // interferes with the selection and focus shifting done here because
                    // they don't actually close until after we are done.
                    gemCutter.closeRunPopupMenus();
                   
                    // Update the gem which was clicked on.
                    clickGem = partPressed.getDisplayedGem();
                   
                    // Selection state changes depend on keyboard modifiers
                    if (e.isControlDown() && e.isShiftDown()) {
                       
                        // Only worry about the left button here
                        if (SwingUtilities.isLeftMouseButton(e)) {
                           
                            // If the selection anchor is null use the clicked Gem
                            if (shiftSelectionAnchorGem == null) {
                                shiftSelectionAnchorGem = clickGem;
                            }
                           
                            // Select all the Gems from the selection anchor to this Gem
                            // and update the focused Gem.  This selection set should union with
                            // any existing selection set.  DO NOT update the selection anchor
                            Rectangle2D rectangle = getRectangleForDisplayedGems(shiftSelectionAnchorGem, clickGem);
                            tableTop.selectGems(rectangle, SelectMode.SELECT);
                            tableTop.setFocusedDisplayedGem(clickGem);
                        }
                       
                    } else if (e.isControlDown()) {
                        // Do nothing

                    } else if (e.isShiftDown()) {
                       
                        // Only worry about the left button on shift+presses here.  We'll have to
                        // worry about the shift+right button  in the 'on clicking' event
                        if (SwingUtilities.isLeftMouseButton(e)) {
                           
                            // If the shift selection anchor is null use the clicked Gem
                            if (shiftSelectionAnchorGem == null) {
                                shiftSelectionAnchorGem = clickGem;
                            }
                           
                            // Select all the Gems from the selection anchor to this Gem
                            // and update the focused Gem.  This new selection set should REPLACE
                            // any existing selection set.  DO NOT update the selection anchor
                            Rectangle2D rectangle = getRectangleForDisplayedGems(shiftSelectionAnchorGem, clickGem);
                            tableTop.selectGems(rectangle, SelectMode.REPLACE_SELECT);
                            tableTop.setFocusedDisplayedGem(clickGem);
                        }
                       
                    } else {
                        // Gem is selected, all others deselected, unless the Gem
                        // is already selected (in which case this is a NOP for now - it will be handled
                        // by mouseReallyClicked() later on)
                        if (!tableTop.isSelected(clickGem)) {
                           
                            tableTop.selectDisplayedGem(clickGem, true);
                        }

                        // Give the Gem that was just clicked the focus and
                        // reset the selection anchor
                        tableTop.setFocusedDisplayedGem(clickGem);
                        shiftSelectionAnchorGem = clickGem;
                       
                        // If needed forward the mouse event to the vep.
                        if (valueEntryPanelHit(e.getPoint())) {
                            forwardMouseEvent (getValueEntryPanel((ValueGem)clickGem.getGem()), e);
                            dragStartedOverVEP = true;
                        }
                    }
                   
                    //Create the dragList of selected Gems
                    dragList = tableTop.getSelectedDisplayedGems();
                   
                } else if (partPressed instanceof DisplayedPartOutput) {

                    DisplayedPartOutput outPart = (DisplayedPartOutput)partPressed;
                   
                    // If we were in intellicut mode part sink, possibly auto connect it.
                    if (prevIntellicutMode == IntellicutManager.IntellicutMode.PART_INPUT &&
                            tableTop.handleIntellicutAutoConnectGesture(outPart)) {

                        // disable dragging if we autoconnected
                        setDragMode(DragMode.ABORTED);
                        return;
                    }
                   
                    // We could be starting a drag.  Adjust the apparent press point to be the point of the arrow
                    pressedAt = outPart.getConnectionPoint();
                   
                    // Set the source part
                    connectionDragAnchorPart = (DisplayedPartConnectable) partPressed;
                   
                } else if (partPressed instanceof DisplayedPartInput) {
                   
                    DisplayedPartInput sinkPart = (DisplayedPartInput)partPressed;
                   
                    // If we were in intellicut mode part source, possibly auto connect it.
                    if (prevIntellicutMode == IntellicutManager.IntellicutMode.PART_OUTPUT &&
                            tableTop.handleIntellicutAutoConnectGesture(sinkPart)) {

                        // disable dragging if we autoconnected
                        setDragMode(DragMode.ABORTED);
                        return;
                    }
                   
                    // We could be starting a drag.  Adjust the apparent press point to be the connection point
                    pressedAt = sinkPart.getConnectionPoint();
                   
                    // Set the source part
                    connectionDragAnchorPart = (DisplayedPartConnectable) partPressed;
                }
               
            } else {       
                // Nothing hit - deselect everything unless a meta key is pressed
                // Selection state changes depend on shift state
                if (!(e.isShiftDown() || e.isControlDown())) {
                    tableTop.selectDisplayedGem(null, false);
                    tableTop.setFocusedDisplayedGem(null);
                    shiftSelectionAnchorGem = null;
                }  
            }  
        }

        /**
         * Surrogate method for mouseClicked.  Called only when our definition of click occurs.
         * @param e MouseEvent the relevant event
         * @return boolean true if the click was a double click
         */
        public boolean mouseReallyClicked(MouseEvent e) {
           
            // call the superclass method
            boolean doubleClicked = super.mouseReallyClicked(e);

            // If needed forward the mouse event to the vep.
            if (valueEntryPanelHit(e.getPoint())) {
                forwardMouseEvent (getValueEntryPanel((ValueGem)clickGem.getGem()), e);
                return doubleClicked;
            }
           
            // Test if we hit any part of the gem
            DisplayedPart partClicked = tableTop.getGemPartUnder(e.getPoint());
            if (partClicked != null) {
               
                // if the user double left clicks on an input part, burn if appropriate
                if (SwingUtilities.isLeftMouseButton(e) && doubleClicked) {
                    tableTop.getBurnManager().handleBurnInputGesture(partClicked);
                }
               
                // Get the gem which was clicked on
                DisplayedGem displayedGemClicked = tableTop.getGemUnder(e.getPoint());
                Gem gemClicked = displayedGemClicked.getGem();

                // If the part clicked was a body part... could be one of several things to do
                if (partClicked instanceof DisplayedPartBody) {
                   
                    // If this is a double left click we want to open the editors for the CodeGem and CollectorGem.
                    // todoSN - should the ValueGem editor be given focus here?
                    if (doubleClicked && SwingUtilities.isLeftMouseButton(e)) {
                       
                        if (gemClicked instanceof CodeGem) {
                            tableTop.showCodeGemEditor((CodeGem)displayedGemClicked.getGem(), true);
                           
                        } else if (gemClicked instanceof CollectorGem) {
                            if (tableTop.isSelected(displayedGemClicked)) {
                                tableTop.displayLetNameEditor((CollectorGem)gemClicked);
                            }
                        } else if (gemClicked instanceof RecordFieldSelectionGem) {
                            Action action = getChangeRecordSelectionFieldAction(gemClicked);
                            if (action.isEnabled()) {
                                // action will only be enabled when RecordFieldSelection Gem can be edited,
                                // ie. when it is not connected to any broken gems
                                tableTop.displayRecordFieldSelectionEditor((RecordFieldSelectionGem)gemClicked);
                            }
                        }
                        else if (gemClicked instanceof RecordCreationGem){
                            // find the field that was clicked on
                            int fieldIndex = displayedGemClicked.getDisplayedGemShape().inputNameTagHit(e.getPoint());

                            if (fieldIndex != -1) {
                                FieldName fieldToRename = ((RecordCreationGem)gemClicked).getFieldName(fieldIndex);
                                Action action = getRenameRecordFieldAction((RecordCreationGem)gemClicked, fieldToRename.getCalSourceForm());

                                if (action.isEnabled()) {

                                    tableTop.displayFieldRenameEditor((RecordCreationGem)gemClicked, fieldToRename);
                                }
                            }
                        }
                    }
                   
                    // Handle the single click cases
                    // NOTE: The CTRL+SHIFT modifiers work the same as just the SHIFT modifier so we
                    //        don't need to do anything special for the combination, however it is important
                    //        that the SHIFT portion of the if statement come before the CTRL portion so
                    //        that it gets executed when the SHIFT+CTRL combo is used.
                    if (e.isShiftDown()) {
                       
                        // Only worry about the right button here. 
                        if (SwingUtilities.isRightMouseButton(e)) {
                            // If the clicked gem is not selected then singleton select it, but do NOT give it focus.
                            if (!tableTop.isSelected(clickGem)) {
                                tableTop.selectDisplayedGem(clickGem, true);
                            }
                           
                            // todoSN - Right now we don't have a permanent focused Gem so if it is null here assign one
                            if (tableTop.getFocusedDisplayedGem() == null) {
                                tableTop.setFocusedDisplayedGem(clickGem);
                            }
                        }                           
                       
                    } else if (e.isControlDown()) {

                        // Worry about the left button here. 
                        if (SwingUtilities.isLeftMouseButton(e)) {
                            // Toggle the selection state of this Gem, give focus to it and make it the shift selection anchor
                            tableTop.toggleSelected(clickGem);
                            tableTop.setFocusedDisplayedGem(clickGem);
                            shiftSelectionAnchorGem = clickGem;
                        }   
                       
                    } else {
                        // There were no modifiers!  Only worry about the left button here.  The right mouse
                        // button will trigger the context menu on the button release event.
                        if (SwingUtilities.isLeftMouseButton(e)) {

                            // When there is a selection set and the clicked Gem is in that set
                            // we need to un-select all but the clicked gem here.
                            DisplayedGem[] selectedGems = tableTop.getSelectedDisplayedGems();
                           
                            if (selectedGems != null && selectedGems.length > 1) {
                                for (final DisplayedGem dGem : selectedGems) {

                                    if (dGem != displayedGemClicked) {
                                        tableTop.selectDisplayedGem(dGem, false);
                                    }
                                }
                            }
                        }
                    }
                }
            }
           
            return doubleClicked;
        }

        /**
         * Surrogate method for mouseDragged.  Called only when our definition of drag occurs.
         * Note that the setup for the transition from pressed to dragged is carried out in the mouseDragged() method.
         * @param e MouseEvent the relevant event
         * @param where Point the (possibly adjusted from e) coordinates of the drag
         * @param wasDragging boolean True: this is a continuation of a drag.  False: first call upon transition
         * from pressed to drag.
         */
        public void mouseReallyDragged(MouseEvent e, Point where, boolean wasDragging) {

            if (isGemDragging()) {
               
                // expand the tabletop if necessary.  This also translates "where" into the new coordinates.
                checkExpand(where);
               
                // Must do the target gem relocating here. Or else we will have repainting problems later.
                tableTop.checkTargetDockLocation();

                // update the drag position, making sure that the new point is visible.
                Rectangle visibleRect = new Rectangle(where);
                updateDragPosition(where, visibleRect, wasDragging);
               
            } else if (dragMode == TableTopDragMode.CONNECTING || dragMode == TableTopDragMode.DISCONNECTING) {
               
                // update the apparent click point in case the source moved (eg. the connected gem morphs)
                pressedAt = connectionDragAnchorPart.getConnectionPoint();

                // update the drag position, making sure that the new point is visible.
                Rectangle visibleRect = new Rectangle(where);
                updateDragPosition(where, visibleRect, wasDragging);
               
                // update the tabletop state to take into account the present drag position while connecting
                changeStateForConnecting(where);

                // undo autoburns if we're dragging away from an output
                DisplayedPart displayedPartUnder = tableTop.getGemPartUnder(where);
                Gem autoBurnLastGem = tableTop.getBurnManager().getAutoburnLastGem();
                if (dragMode == TableTopDragMode.DISCONNECTING &&
                        connectionDragAnchorPart instanceof DisplayedPartInput &&
                        autoBurnLastGem != null &&
                        (displayedPartUnder == null || displayedPartUnder.getGem() != autoBurnLastGem) &&
                        tableTop.getBurnManager().getAutoburnLastResult() == AutoburnLogic.AutoburnAction.BURNED) {
                    tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(autoBurnLastGem);
                }

                // clear the status message
                gemCutter.getStatusMessageDisplayer().clearMessage(TableTopPanel.this);
               
            } else if (dragMode == TableTopDragMode.SELECTING) {
               
                // update the drag position, making sure that the new point is visible.
                Rectangle visibleRect = new Rectangle(where);
                updateDragPosition(where, visibleRect, wasDragging);

                // Determine the next drag sel mode
                lastSelectMode = getSelectModeForEvent(e);
               
            }
        }

        /**
         * Invoked when a mouse button has been released on a component.
         */
        public void mouseReleased(MouseEvent e) {
           
            // show popup menu if appropriate
            if (!isUsefulDragMode(dragMode)) {
                maybeShowPopup(e);
            }
           
            // Ignore clicks unless we're editing
            if (gemCutter.getGUIState() != GemCutter.GUIState.EDIT) {
                return;
            }

            // If needed forward the mouse event to the vep.
            if (valueEntryPanelHit(e.getPoint())) {
                forwardMouseEvent (getValueEntryPanel((ValueGem)clickGem.getGem()), e);
            }

            try {
                // defer to the superclass method to do click/drag
                super.mouseReleased(e);        

            } finally {
                // Clear press/drag state
                resetMouseStates();

                // Restore focus if we took it away
                tableTop.restoreFocus();
               
                // resize the tabletop if necessary, to take into account any new gem state
                tableTop.resizeForGems();
               
                // if the menu was not to be shown due to a drag, we can show it again now
                // that the mouse has been released
                popupShouldShow = true;
            }
        }

        /**
         * Forward the given MouseEvent to the specified component by generating a new fake
         * event and posting it on the system event queue.
         * @param parent the parent component to forward the event to
         * @param e the MouseEvent to forward
         */
        private void forwardMouseEvent(Component parent, MouseEvent e) {

            popupShouldShow = false;
           
            EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue();
            Point newLocation = SwingUtilities.convertPoint((Component) e.getSource(), e.getPoint(), parent);
            Component newSource = parent.getComponentAt(newLocation);

            if (newSource == null) {
                // Happens if you click on the border of a component.
                return;
            }

            newLocation = SwingUtilities.convertPoint(parent, newLocation, newSource);
           
            queue.postEvent(new MouseEvent(newSource,
                    e.getID(),
                    System.currentTimeMillis(),
                    e.getModifiers(),
                    newLocation.x,
                    newLocation.y,
                    e.getClickCount(),
                    e.isPopupTrigger(),
                    e.getButton()));
        }
       
        /**
         * Determine if a value entry panel is under the given point.
         * @param point the click point
         * @return true if the value entry panel of a displayed value gem was hit.
         **/
        private boolean valueEntryPanelHit(Point point) {
            DisplayedGem displayedGem = tableTop.getGemUnder(point);
           
            return !isUsefulDragMode(getDragMode()) &&
                displayedGem != null &&
                displayedGem.getGem() instanceof ValueGem &&
                getValueEntryPanel((ValueGem)displayedGem.getGem()).getBounds().contains(point);
        }

        /**
         * Repaint the drag ghost (if any)
         */
        private void repaintDrag(Graphics2D g2d) {

//            class DragRepainter implements Runnable{
//
//                Graphics2D g2d;
//                RedrawInfo redrawInfo;
//
//                DragRepainter(Graphics2D g2d, RedrawInfo redrawInfo) {
//                    this.g2d = g2d;
//                    this.redrawInfo = redrawInfo;       // save a copy of the present redraw info
//                }
//                public void run() {
//                    // only repaint the drag connection line if there hasn't been another
//                    // drag ghost painted in the meantime (ie. redraw info hasn't been updated..)
//                    if (TableTopMouseHandler.this.redrawInfo == redrawInfo) {
//                        drawDragGhost(DrawAction.REDRAW, null, g2d);
//                    }
//                }
//            }

            // Say that the last drag ghost is in the undrawn state so that intervening drags will only
            // draw (rather than first undraw).
            lastDragClipArea = null;

            // Invoke later because we want other painting to finish before painting the drag.
            // Otherwise, for some reason (maybe because of double buffering?) the drag ghost disappears
            // when repaint returns.
            //SwingUtilities.invokeLater(new DragRepainter(g2d, redrawInfo));
           
        }

        /**
         * Reset mouse states
         */
        void resetMouseStates(){
            // Exit the pressed state
            pressedAt = null;
           
            // Dragging is finished.  Reset dragPos and dragMode
            dragPos = null;
            setDragMode(DragMode.NOTDRAGGING);

            // Reset the cursor
            setCursor(null);

            // invalidate autoburn attempt state
            tableTop.getBurnManager().invalidateAutoburnState();
        }

        /**
         * Update the state of the tabletop to reflect the new mouse drag position.
         * This takes care of undrawing and drawing old and new drag ghosts, as well as updating dragPos, and prevDragPos and
         * lastScrollDistanceX/Y if necessary.
         * @param newDragPos Point The new mouse drag position
         * @param visibleRect Rectangle The rectangle which we would like to see after the position update
         * @param wasDragging boolean if we were dragging before (and therefore must undraw the old drag ghost)
         */
        private void updateDragPosition(Point newDragPos, Rectangle visibleRect, boolean wasDragging) {
           
            // signal that we're painting
            isPainting++;
           
            // get the current graphics context
            Graphics2D g2d = (Graphics2D)getGraphics();
           
            // Turn off ghosts at last position. 
            // This is performed before scrollRect..() to avoid having a ghost in the middle of the screen.
            if (wasDragging){
                drawDragGhost(DrawAction.UNDRAW, SelectMode.REPLACE_SELECT, g2d);
            }

            // ensure the new ghost is visible     
            scrollRectToVisible(visibleRect);

            // This position is new drag position
            dragPos = newDragPos;
           
            // Turn on ghosts at this position
            g2d.setClip(getVisibleRect());
            drawDragGhost(DrawAction.DRAW, SelectMode.REPLACE_SELECT, g2d);
           
            // dispose our graphics object
            g2d.dispose();
           
            // we're no longer painting
            isPainting--;
        }
    }
   
    /**
     * Default constructor for this class.
     * @param tableTop
     * @param gemCutter
     */
    TableTopPanel(TableTop tableTop, GemCutter gemCutter) {
       
        this.tableTop = tableTop;
        this.gemCutter = gemCutter;
        this.valueGemPanelMap = new WeakHashMap<ValueGem, ValueEntryPanel>();
        this.gemPainter = new TableTopGemPainter(tableTop);
        this.isPainting = 0;

        // Register listeners for a number of event classes
        this.tableTopMouseHandler = new TableTopMouseHandler();
        this.runModeMouseHandler = new RunModeMouseHandler();
       
        addMouseListener(tableTopMouseHandler);
        addMouseMotionListener(tableTopMouseHandler);
        addComponentListener(new TableTopComponentListener());
        addKeyListener(new KeyStrokeHandler());
       
        // Register a drop target listener
        DropTargetListener dropTargetListener = new TableTopDragAndDropHandler();
        setDropTarget(new DropTarget(this, dropTargetListener));
       
        setToolTipText(GemCutter.getResourceString("TableTopToolTip"));
        setFocusCycleRoot(true);
        setFocusTraversalPolicy(new LayoutFocusTraversalPolicy());
    }
   
   
    /**
     * Enable or disable mouse events on the TableTop, allowing or disallowing gems from being moved.
     * @param notRunning whether to enable or disable mouse events
     */
    void enableMouseEvents(boolean notRunning) {
        if (notRunning) {
            removeMouseListener(runModeMouseHandler);
            addMouseListener(tableTopMouseHandler);
            addMouseMotionListener(tableTopMouseHandler);

        } else {
            addMouseListener(runModeMouseHandler);
            removeMouseListener(tableTopMouseHandler);
            removeMouseMotionListener(tableTopMouseHandler);
        }
    }
   
    /**
     * Handle the addition of a value gem to the tableTop.
     * This adds a value entry panel for use in editing the value for a value gem.
     * @param valueGem the value gem in question.
     */
    void handleValueGemAdded(ValueGem valueGem) {
       
        ValueEntryPanel valueEntryPanel = getValueEntryPanel(valueGem);
       
        // Initially not visible until the user selects the gem.
        valueEntryPanel.setVisible(false);
       
        // Update the position of the panel.
        updateValueGemPanelLocation(valueGem);

        // Add the panel to this component..
        TableTopPanel.this.add(valueEntryPanel);

        // Have to reset closing flag if the gem has been previously placed (eg. add gem, undo, add gem again).
        valueEntryPanel.setEditorIsClosing(false);

        // Add to the hierarchy manager.
        gemCutter.getValueEditorHierarchyManager().addTopValueEditor(valueEntryPanel);
        TableTopPanel.this.revalidate();
    }
   
    /**
     * Handle the removal of a value gem from the tableTop.
     * @param valueGem the value gem which was removed.
     */
    void handleValueGemRemoved(ValueGem valueGem) {
        gemCutter.getValueEditorHierarchyManager().removeValueEditor(getValueEntryPanel(valueGem), true);
    }
   
    /**
     * Handle the situation where a value gem was moved.
     * @param valueGem the value gem which was moved.
     */
    void handleValueGemMoved(ValueGem valueGem) {
        updateValueGemPanelLocation(valueGem);
    }
   
    /**
     * Update the location of the value gem's value entry panel to match the gem location.
     * @param valueGem the value gem whose panel should have its location updated.
     */
    private void updateValueGemPanelLocation(ValueGem valueGem) {
        Point currentLocation = tableTop.getDisplayedGem(valueGem).getLocation();
        int vepX = currentLocation.x + DisplayConstants.BEVEL_WIDTH_X + 1;
        int vepY = currentLocation.y + DisplayConstants.BEVEL_WIDTH_Y + 1;
        getValueEntryPanel(valueGem).setLocation(new Point(vepX, vepY));
    }
   
    /**
     * Get the value entry panel used to edit the value for a given value gem.
     * @param valueGem the value gem in question.
     * @return the value entry panel used to edit the value gem's value.
     */
    ValueEntryPanel getValueEntryPanel(final ValueGem valueGem) {

        // Lazily create the value panel on demand.
        if (!valueGemPanelMap.containsKey(valueGem)) {
           
            final ValueEditorHierarchyManager valueEditorHierarchyManager = gemCutter.getValueEditorHierarchyManager();
            ValueEditorManager valueEditorManager = valueEditorHierarchyManager.getValueEditorManager();
            ValueNode valueNode = valueGem.getValueNode();
   
            // Create the value entry panel.
            final ValueEntryPanel valueEntryPanel =
                (ValueEntryPanel)valueEditorManager.getValueEditorDirector().getRootValueEditor(valueEditorHierarchyManager,
                                                                                                valueNode, null, 0, null);
           
            // Add it to the map.
            valueGemPanelMap.put(valueGem, valueEntryPanel);
           
            // add a listener to propagate changes in the value gem to the VEP.
            valueGem.addValueChangeListener(new ValueGemChangeListener() {
                public void valueChanged(ValueGemChangeEvent e) {
                    ValueGem valueGem = (ValueGem)e.getSource();
                    valueEditorHierarchyManager.collapseHierarchy(valueEntryPanel, false);
                    valueEntryPanel.changeOwnerValue(valueGem.getValueNode());
                    valueEntryPanel.setSize(valueEntryPanel.getPreferredSize());
                    valueEntryPanel.revalidate();
                }
            });
   
            // Set size of the panel.
            valueEntryPanel.setSize(valueEntryPanel.getPreferredSize());
           
            // Add a listener to propagate changes in the VEP to the value gem.
            valueEntryPanel.addValueEditorListener(new ValueEditorAdapter() {
                public void valueCommitted(ValueEditorEvent evt) {
                    ValueNode oldValue = evt.getOldValue();
                    ValueNode newValue = ((ValueEntryPanel)evt.getSource()).getValueNode();
   
                    if (!oldValue.sameValue(newValue)) {
                        valueGem.changeValue(newValue);
                    }
                }
            });
   
            // Add a listener so that a change in the size of the VEP will trigger a change the size of the displayed gem.
            valueEntryPanel.addComponentListener(new ComponentAdapter() {
   
                // change the size of the displayed value gem if the VEP is resized
                public void componentResized(ComponentEvent e) {
                    tableTop.getDisplayedGem(valueGem).sizeChanged();
                }
   
                // Re-position displayed gem if the VEP moves
                // This will happen if the VEP size changes (as a result of a type change), and its size is clamped to the parent's bounds.
                // eg. stick a new VEP on the right edge of the TableTop, and change its type to String.
                public void componentMoved(ComponentEvent e) {
                    Point vepLocation = valueEntryPanel.getLocation();
                    int newX = vepLocation.x - DisplayConstants.BEVEL_WIDTH_X - 1;
                    int newY = vepLocation.y - DisplayConstants.BEVEL_WIDTH_Y - 1;
   
                    Point newPoint = new Point(newX, newY);
                    tableTop.getDisplayedGem(valueGem).setLocation(newPoint);
                }
            });
   
            // Set the vep's context for type switching.
            valueEntryPanel.setContext(new ValueEditorContext() {
                public TypeExpr getLeastConstrainedTypeExpr() {
                    return tableTop.getGemGraph().getLeastConstrainedValueType(valueGem, tableTop.getTypeCheckInfo());
                }
            });
           
            // Set it up so that VEP commits are handled as user edits.
            valueEntryPanel.addValueEditorListener(new ValueEditorAdapter() {
                public void valueCommitted(ValueEditorEvent evt) {
                    tableTop.handleValueGemCommitted(valueGem, evt.getOldValue(), valueEntryPanel.getValueNode());
                }
            });
        }

        return valueGemPanelMap.get(valueGem);
    }
   
    /**
     * Sets whether or not the ValueGems should be enabled/editable.
     * @param enable true to enable, false to disable.
     */
    void setValueGemsEnabled(boolean enable) {
        Set<Gem> gemSet = tableTop.getGemGraph().getGems();
       
        for (final ValueGem valueGem : valueGemPanelMap.keySet()) {
            if (gemSet.contains(valueGem)) {
                ValueEntryPanel vep = getValueEntryPanel(valueGem);
                vep.setEditable(enable);

                // Make sure the value panels change colour accordingly.
                repaint(vep.getBounds());
            }
        }
    }
   
    /**
     * Revalidates the value gems on the tabletop after a connection
     * (eg. if you connect an add gem to 2 value gems, then you specialize one,
     *   then this method will ensure that the other's appearance gets updated as well
     */
    void revalidateValueGemPanels() {
        Set<Gem> gemSet = tableTop.getGemGraph().getGems();
       
        for (final ValueGem valueGem : valueGemPanelMap.keySet()) {
            if (gemSet.contains(valueGem)) {
                ValueEntryPanel valueEntryPanel = getValueEntryPanel(valueGem);
               
                valueEntryPanel.refreshDisplay();
   
                // If a value gem is connected to a broken code gem, its least constrained type
                // will be null. In that case disable editing it.
                valueEntryPanel.setEditable(valueEntryPanel.getContext().getLeastConstrainedTypeExpr() != null);
   
                // Repaint the value gem ghost image to display the right value entry panel.
                repaint(tableTop.getDisplayedGem(valueGem).getBounds());
            }
        }
    }   

    /**
     * Hides the popup menu
     */
    void hidePopup() {
        if (currentPopupMenu != null) {
            currentPopupMenu.setVisible(false);
        }
    }
   
    /**
     * Sets the background for the tabletop
     * @param image
     */
    void setBackground(BufferedImage image) {
        this.backgroundImage = image;
        repaint();
    }

    /**
     * Paint the TableTop.
     * @param g Graphics
     */
    public void paintComponent(Graphics g) {
       
        if (backgroundImage != null) {
           
            // Paint a tiled image         
            Rectangle bounds = g.getClipBounds();

            // Determine which tiled instances intersect with bounds and draw them
            int imageWidth = backgroundImage.getWidth();
            int imageHeight = backgroundImage.getHeight();
            backgroundImageOriginOffsetX = backgroundImageOriginOffsetX % imageWidth;
            backgroundImageOriginOffsetY = backgroundImageOriginOffsetY % imageHeight;

            if (backgroundImageOriginOffsetX > 0) {
                backgroundImageOriginOffsetX -= imageWidth;
            }
            if (backgroundImageOriginOffsetY > 0) {
                backgroundImageOriginOffsetY-=imageHeight;
            }
           
            int offsetX = (bounds.x / imageWidth) * imageWidth + backgroundImageOriginOffsetX;
            int offsetY = (bounds.y / imageHeight) * imageHeight + backgroundImageOriginOffsetY;

            for (int yRegistration = offsetY; yRegistration < bounds.y + bounds.height; yRegistration += imageHeight) {
                for (int xRegistration = offsetX; xRegistration < bounds.x + bounds.width; xRegistration += imageWidth) {
                    g.drawImage(backgroundImage, xRegistration, yRegistration, null);
                }
            }
           
        } else {
            // Just draw a plain background.
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, getWidth(), getHeight());
        }

        // Signal that we're still painting.
        isPainting++;

        // Get a Graphics2D object to paint some of the components with
        Graphics2D g2d = (Graphics2D) g;

        // Call the graph to paint itself into this graphics context
        // All Gems get painted, and the links established between their 'ports' (output and inputs)
        // Links will be colourised depending on their validity and type compatibility
        paintGemGraph(g2d);

        // paint the drag ghost
        tableTopMouseHandler.repaintDrag(g2d);

        // we're no longer painting
        isPainting--;
    }

    /**
     * Paint this GemGraph within the graphics context passed in.
     * @param g2d Graphics2D the graphics context
     */
    public void paintGemGraph(Graphics2D g2d) {
       
        // Paint each Gem in the 'graph', and the edges between them
       
        // Start with the connections
        for (final DisplayedConnection displayedConnection : tableTop.getDisplayedConnections()) {
            gemPainter.paintConnection(displayedConnection, g2d);
        }
       
        // Now the intellicut lines
        tableTop.getIntellicutManager().paintIntellicutLines(g2d);
       
        // Now the Gems themselves
        for (final DisplayedGem displayedGem : tableTop.getDisplayedGems()) {
            gemPainter.paintGem(displayedGem, g2d);
        }
    }

    /**
     * {@inheritDoc}
     * Overriden to spot when updates are forced and cause the overview to update.
     */
    public void repaint() {
        super.repaint();
       
        // Update the overview
        gemCutter.getOverviewPanel().repaint();
    }
   
    /**
     * {@inheritDoc}
     * Overriden to spot when updates are forced and cause the overview to update.
     */
    public void repaint(Rectangle rect) {  
        super.repaint(rect);
       
        // Update the overview
        gemCutter.getOverviewPanel().repaint();
    }
   
    /**
     * Resets the state of the mouse handler, painting and VEP's
     */
    void resetState() {
       
        // reset mouse states (in case there was an exception)
        tableTopMouseHandler.resetMouseStates();
       
        // Remove children, in case there are any orphaned VEPs
        removeAll();
       
        // Painting stuff
        isPainting = 0;
        repaint();
    }
   
    /**
     * If the mouse event is a popup trigger and a popup menu should show, then this method
     * will display the appropriate popup menu for the location clicked on.
     * @param e the related mouse event
     */
    void maybeShowPopup(MouseEvent e) {
       
        if (!popupShouldShow || !e.isPopupTrigger()) {
            return;
        }
       
        if (gemCutter.getGUIState() == GUIState.RUN) {
            currentPopupMenu = getRunModePopupMenu();
            currentPopupMenu.show(this, e.getX(), e.getY());
            return;
        }

        currentPopupLocation = e.getPoint();
       
        DisplayedGem gem = tableTop.getGemUnder(currentPopupLocation);
        DisplayedPart part = tableTop.getGemPartUnder(currentPopupLocation);
       
        if (part instanceof DisplayedPartBody) {
           
            currentPopupMenu = getGemPopupMenu(gem, true);
           
        } else if (part instanceof DisplayedPartConnectable) {
           
            if (e.isControlDown()) {
                tableTop.maybeStartIntellicutMode(part);
            } else {
                currentPopupMenu = getGemPartPopupMenu((DisplayedPartConnectable) part, true);
            }
           
        } else if (part == null) {
           
            if (e.isControlDown()) {
                tableTop.getIntellicutManager().startIntellicutModeForTableTop(new Rectangle(currentPopupLocation), currentPopupLocation);
            } else {
                currentPopupMenu = getNonGemPopupMenu(true);
            }
           
        } else {
            throw new IllegalStateException("unknown part for popup menu");
        }
       
        currentPopupMenu.show(this, e.getX(), e.getY());
    }

    /**
     * @return the popup menu to show for the table top while the GemCutter is in run mode
     */
    JPopupMenu getRunModePopupMenu() {
       
        JPopupMenu runModePopupMenu = new JPopupMenu();
       
        runModePopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getResumeRunAction()));
        runModePopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getStopAction()));
        runModePopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getResetAction()));

        return runModePopupMenu;
    }
   
    /**
     * @return the JPopupMenu to show when the user right clicks on the table top, not on a gem or part
     * @param forTableTop whether the popup menu is for the table top and should include table top specific items
     *
     * <BR><BR>todoFW: find a better way to share popup menu code with the TableTopExplorer
     */
    JPopupMenu getNonGemPopupMenu(boolean forTableTop) {
       
        JPopupMenu nonGemPopupMenu = new JPopupMenu();

        final JMenuItem addReflectorMenuItem = GemCutter.makeNewMenuItem(getAddReflectorAction());
        final JMenu addOtherReflectorMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_AddOtherReflector"));

        if (forTableTop) {
            nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddGemAction()));
        }
       
        nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddValueGemAction()));
        nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddCodeGemAction()));
        nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddCollectorAction()));
        nonGemPopupMenu.add(addReflectorMenuItem);
        nonGemPopupMenu.add(addOtherReflectorMenu);
        nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddRecordCreationGemAction()));
        nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddRecordFieldSelectionGemAction()));
        nonGemPopupMenu.addSeparator();
        nonGemPopupMenu.add(getCopySpecialMenu());
        nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getPasteGemsAction()));
       
        if (forTableTop) {
            nonGemPopupMenu.addSeparator();
            nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getTidyTableTopAction()));
            nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getFitTableTopAction()));
        }
       
        // This listener enables/disables the add emitter item and resets the popup location
        nonGemPopupMenu.addPopupMenuListener(new PopupMenuListener() {
           
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
               
                // Get the names of all the collectors.
                List<String> sortedReflectorNames = new ArrayList<String>();
                Map<String, CollectorGem> nameToCollectorMap = new HashMap<String, CollectorGem>();
               
                for (final Gem gem : gemCutter.getTableTop().getGemGraph().getCollectors()) {
                    CollectorGem collectorGem = (CollectorGem)gem;
                    nameToCollectorMap.put(collectorGem.getUnqualifiedName(), collectorGem);
                    sortedReflectorNames.add(collectorGem.getUnqualifiedName());
                }
               
                Collections.sort(sortedReflectorNames, String.CASE_INSENSITIVE_ORDER);
               
                // Update the text of the add reflector menu item
                CollectorGem reflectorCollector = gemCutter.getCollectorForAddingReflector();
                addReflectorMenuItem.setEnabled(reflectorCollector != null);
               
                if (reflectorCollector != null) {
                    addReflectorMenuItem.setText(GemCutter.getResourceString("PopItem_AddReflectorFor") + reflectorCollector.getUnqualifiedName());
                } else {
                    addReflectorMenuItem.setText(GemCutter.getResourceString("PopItem_AddReflector"));
                }
                addOtherReflectorMenu.removeAll();
                addOtherReflectorMenu.setEnabled(sortedReflectorNames.size() > 0);               
                for (final String reflectorName : sortedReflectorNames) {
                    reflectorCollector = nameToCollectorMap.get(reflectorName);
                    addOtherReflectorMenu.add(GemCutter.makeNewMenuItem(new AddReflectorAction(reflectorCollector)));
                }
            }
           
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            }
           
            public void popupMenuCanceled(PopupMenuEvent e) {
            }
        });
       
        return nonGemPopupMenu;
    }
   
    /**
     * @return the "Copy Special" menu, identical to that of the gem cutter.
     */
    private JMenu getCopySpecialMenu() {
        JMenu originalMenu = gemCutter.getCopySpecialMenu();
        JMenu copySpecialMenu = GemCutter.makeNewMenu(originalMenu.getText());
        int items = originalMenu.getItemCount();
        for (int i = 0; i < items; i++) {
            copySpecialMenu.add(GemCutter.makeNewMenuItem(originalMenu.getItem(i).getAction()));
        }
        return copySpecialMenu;
    }

    /**
     * @param part the part that was clicked on
     * @param forTableTop whether the menu is for the table top and should include table top specific items
     * @return the JPopupMenu to show for the given part
     */
    JPopupMenu getGemPartPopupMenu(DisplayedPartConnectable part, boolean forTableTop) {
       
        JPopupMenu partPopupMenu = new JPopupMenu();
        JSeparator separator = new JSeparator();
       
        if (forTableTop) {
            partPopupMenu.add(GemCutter.makeNewMenuItem(getIntellicutAction(part)));
            partPopupMenu.add(separator);
        }
       
        if (part instanceof DisplayedPartInput) {
            DisplayedPartInput inputPart = (DisplayedPartInput) part;
           
            if (!inputPart.isConnected()) {
                partPopupMenu.add(GemCutter.makeNewMenuItem(getConnectValueGemAction(inputPart)));
               
                if (!(inputPart.getPartInput().getGem() instanceof CollectorGem)) {
                   
                    if (inputPart.getPartInput().isBurnt()) {
                        partPopupMenu.add(GemCutter.makeNewMenuItem(getUnburnAction(inputPart)));
                       
                    } else {
                        partPopupMenu.add(GemCutter.makeNewMenuItem(getBurnAction(inputPart)));
                    }
                }
            }
           
            // Create popup menu item for retargeting inputs.
            final JMenu retargetInputMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_RetargetInput"));
            partPopupMenu.add(retargetInputMenu);
           
            // Get the targetable collectors, enable the menu item if not empty and if the input is not connected.
            List<CollectorGem> targetableCollectorList = getTargetableCollectors(inputPart.getPartInput());
            retargetInputMenu.setEnabled(!targetableCollectorList.isEmpty() && !inputPart.isConnected());
           
            CollectorGem currentArgTarget = GemGraph.getInputArgumentTarget(inputPart.getPartInput());

            // Create submenu items for retargeting to an enclosing collector.
            for (final CollectorGem targetableCollector : targetableCollectorList) {
               
                // Make the new menu item.
                JMenuItem newMenuItem = GemCutter.makeNewMenuItem(getRetargetInputArgumentAction(inputPart.getPartInput(), targetableCollector));
               
                // Enable if its not already targeted to this collector.
                newMenuItem.setEnabled(targetableCollector != currentArgTarget);
               
                // Add the menu item.
                retargetInputMenu.add(newMenuItem);
            }

           
        } else if (part instanceof DisplayedPartOutput) {
            DisplayedPartOutput outputPart = (DisplayedPartOutput) part;
           
            if (!outputPart.isConnected()) {
                partPopupMenu.add(GemCutter.makeNewMenuItem(getConnectCollectorAction(outputPart)));
            }
        }
       
        if (part.isConnected()) {
            partPopupMenu.add(GemCutter.makeNewMenuItem(getDisconnectAction(part)));         
            partPopupMenu.add(GemCutter.makeNewMenuItem(getSplitConnectionAction(part)));
        }
       
        // If there is nothing after the separator then remove it.
        if (partPopupMenu.getComponentCount() == 2) {
            partPopupMenu.remove(separator);
        }
       
        return partPopupMenu;
    }
   
    /**
     * @param partInput an input
     * @return Map from Collector gem which can be targeted to the collector gem to which the collector
     *   at the root of the input's gem subtree must be retargeted if the input is retargeted to the first collector gem.
     */
    private List<CollectorGem> getTargetableCollectors(PartInput partInput) {
       
        // Get the collector gem at the root of the gem tree to which the input is connected.
        CollectorGem inputGemRoot = partInput.getGem().getRootCollectorGem();
       
        // Can't retarget if the root is not a collector gem.
        if (inputGemRoot == null) {
            return Collections.emptyList();
        }

        // The input can be targeted to the root gem, or any of its enclosing collectors.
        // So, the targetable collectors are the root, any collectors targetable by the root, and any collectors enclosing those.
       
        // We have to ignore the input under consideration, otherwise the call to getTargetableCollectors() will see that the argument
        //   is targeting whatever it's targeting, and use that as a constraint.
        Set<CollectorGem> targetableCollectorSet = new HashSet<CollectorGem>();
        for (final CollectorGem targetableCollector  : getTargetableCollectors(inputGemRoot, null, partInput)) {
            targetableCollectorSet.addAll(GemGraph.obtainEnclosingCollectors(targetableCollector));
        }

        // The input can always be targeted at the root gem...
        targetableCollectorSet.add(inputGemRoot);
       
       
        // Convert to an array, then sort.
        // First, the target gem.  Then, all other collectors, ordered alphabetically by name.
        CollectorGem[] targetableCollectorArray =
            targetableCollectorSet.toArray(new CollectorGem[targetableCollectorSet.size()]);
       
        final CollectorGem gemGraphTarget = tableTop.getGemGraph().getTargetCollector();
        Arrays.sort(targetableCollectorArray, new Comparator<CollectorGem>() {

            public int compare(CollectorGem o1, CollectorGem o2) {
                if (o1 == gemGraphTarget) {
                    return -1;
                }
                if (o2 == gemGraphTarget) {
                    return 1;
                }
                return o1.getUnqualifiedName().compareTo(o2.getUnqualifiedName());
            }
        });
       
        // Return as a list.
        return Arrays.asList(targetableCollectorArray);
    }
   
    /**
     * @param collectorGem a collector gem
     * @param collectorsToCheck if non-null, only the collectors in this set will be checked if they are targetable.
     *   If null, all collectors in the gem graph will be checked.
     * @param inputToIgnore the input to exclude from consideration, if any.  Null if none.
     * @return the collector gems to which the given collector gem may validly be targeted.
     */
    private List<CollectorGem> getTargetableCollectors(CollectorGem collectorGem, Set<CollectorGem> collectorsToCheck, PartInput inputToIgnore) {
       
        // Targetable collectors must satisfy a set of conditions:
        // Collectors:
        //   No circular collector dependencies.
        //   If there are any reflectors attached to the same subtree as reflectors for the retargeting collector gem,
        //     the collector at the root of the tree must be able to access the definitions for those reflectors, as well
        //     as the definition of the retargeting collector.  If there is no collector at the root of the tree,
        //     it must be possible to create a collector which can see all definitions.
        // Arguments:
        //   Arguments on dependee trees which target collectors at outer scopes must still be able to target those collectors after retarget.

        // Can't retarget the target gem.
        CollectorGem targetCollector = collectorGem.getTargetCollectorGem();
        if (targetCollector == null) {
            return Collections.emptyList();
        }
       
        //
        // Get root collectors for emitters for any collectors enclosed by the retargeting collector gem,
        //   where the root collector encloses and is not equal to the collector being retargeted.
        //  ie. the collectors at the roots of the subtrees to which the emitters are connected,
        //   if the root collector encloses and is not equal to the retargeting collector.
        //
        Set<CollectorGem> collectorSet = tableTop.getGemGraph().getCollectors();
        Set<CollectorGem> enclosedCollectorGemReflectorStrictlyEnclosingRootCollectorSet = new HashSet<CollectorGem>();

        for (final Gem gem : collectorSet) {
            CollectorGem nextCollector = (CollectorGem)gem;

            if (collectorGem.enclosesCollector(nextCollector)) {
                for (final ReflectorGem reflectorGem : nextCollector.getReflectors()) {
                    CollectorGem rootCollectorGem = reflectorGem.getRootCollectorGem();
                    if (rootCollectorGem != null && rootCollectorGem != collectorGem && rootCollectorGem.enclosesCollector(collectorGem)) {
                        enclosedCollectorGemReflectorStrictlyEnclosingRootCollectorSet.add(rootCollectorGem);
                    }
                }
            }
        }

        //
        // Get the innermost enclosing collector which has targeting arguments from inputs which exist on subtrees whose root
        //   collectors are enclosed by collectorGem.
        // This will be used to check the condition that arguments on dependee trees which target collectors at enclosing scopes
        //   must still be able to target those collectors.
        //
        CollectorGem innermostEnclosingCollectorWithEnclosedTargetingArguments = null;

        enclosingCollectorLoop:
            for (CollectorGem enclosingCollector = targetCollector; enclosingCollector != null; enclosingCollector = enclosingCollector.getTargetCollectorGem()) {
                for (final PartInput targetArgument : enclosingCollector.getTargetArguments()) {
                    CollectorGem rootCollectorGem = targetArgument.getGem().getRootCollectorGem();

                    // Check for the input to ignore..
                    if (targetArgument == inputToIgnore) {
                        continue;
                    }

                    if (rootCollectorGem == null) {
                        // Shouldn't be able to retarget an input on a subtree not rooted in a collector.
                        GemCutter.CLIENT_LOGGER.log(Level.WARNING, "Targeting input has no root collector: " + targetArgument);
                        continue;
                    }

                    if (collectorGem.enclosesCollector(rootCollectorGem)) {
                        innermostEnclosingCollectorWithEnclosedTargetingArguments = enclosingCollector;
                        break enclosingCollectorLoop;
                    }
                }
            }
       
        // For each subtree, not rooted by a collector, which has a reflector whose
        //   collector is enclosed by collectorGem, the set of targets of (non-collectorGem-enclosed) collectors for the other
        //   reflectors on that subtree.
        //
        // If two collectors have the same target, then they are siblings.
        //
        // This will be used to check the condition that:
        //   For each subtree not rooted in a collector, we must be able to create a collector for the root which can
        //   see all the collectors for the reflectors in that subtree.
        Set<Set<CollectorGem>> nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSets = new HashSet<Set<CollectorGem>>();
        {
            // This could be faster...
            Set<Gem> uncollectedRoots = tableTop.getGemGraph().getRoots();
            uncollectedRoots.removeAll(collectorSet);

            for (final Gem uncollectedRoot : uncollectedRoots) {
                // Get reflectors which exist on the same subtree as the collector's reflector.
                Set<ReflectorGem> subTreeReflectors = UnsafeCast.<Set<ReflectorGem>>unsafeCast(GemGraph.obtainSubTreeGems(uncollectedRoot, ReflectorGem.class));

                // Calculate the set of collectors for reflectors in the subtree, where the collectors are not
                //   enclosed by collector gem.
                Set<CollectorGem> nonEnclosedReflectorCollectorParentSet = new HashSet<CollectorGem>();

                for (final ReflectorGem gem : subTreeReflectors) {
                    CollectorGem subtreeReflectorCollector = gem.getCollector();

                    if (collectorGem.enclosesCollector(subtreeReflectorCollector)) {
                        // Ensure the set is added to the set of collector sets only if there is a reflector collector which
                        //   is enclosed by collector gem.
                        nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSets.add(nonEnclosedReflectorCollectorParentSet);

                    } else {
                        // Add to the set of non-enclosed reflector collectors.
                        CollectorGem subtreeReflectorCollectorTarget = subtreeReflectorCollector.getTargetCollectorGem();
                        if (subtreeReflectorCollectorTarget != null) {
                            nonEnclosedReflectorCollectorParentSet.add(subtreeReflectorCollectorTarget);
                        }
                    }
                }
            }
            // Note that we may have added an empty set..
        }

       
        //
        // Iterate over the collectors in the set to check, checking each to see if it can be targeted.
        //
        if (collectorsToCheck == null) {
            collectorsToCheck = tableTop.getGemGraph().getCollectors();
        }
        Set<CollectorGem> targetableCollectors = new HashSet<CollectorGem>();

        targetableGemCandidateLoop :
            for (final CollectorGem targetableCollectorCandidate : collectorsToCheck) {

                // Guard against circular collector dependencies: disallow retargeting to an enclosed collector (or itself).
                // Check that collectorGem is not an ancestor of targetableGemCandidate.
                if (collectorGem.enclosesCollector(targetableCollectorCandidate)) {
                    continue targetableGemCandidateLoop;
                }

                // Arguments on dependee trees which target collectors at outer scopes (wrt collectorGem) must still be able to target those collectors.
                // In practice, this means we only have to check that (the innermost enclosing collector which has targeting arguments which exist on
                //   subtrees whose root collectors are enclosed by collectorGem) will still enclose collectorGem.
                if (innermostEnclosingCollectorWithEnclosedTargetingArguments != null &&
                        !innermostEnclosingCollectorWithEnclosedTargetingArguments.enclosesCollector(targetableCollectorCandidate)) {

                    continue targetableGemCandidateLoop;
                }

                // Reflectors for collectors enclosed by the retargeting collector gem must be visible to their collector root.
                // So: for a tree with a reflector for such a collector,
                //   if the collector root is also enclosed, relative visibility will not change by retargeting.
                //   if the collector root is not also enclosed, it must have targetableCollectorCandidate as an ancestor or sibling.

                for (final CollectorGem reflectorRootCollector : enclosedCollectorGemReflectorStrictlyEnclosingRootCollectorSet) {
                    boolean reflectorRootCollectorSeesCandidate = false;

                    // Check if they have the same parent.
                    if (reflectorRootCollector.getTargetCollectorGem() == targetCollector) {
                        reflectorRootCollectorSeesCandidate = true;
                    }

                    // Check if the candidate is an ancestor.
                    reflectorRootCollectorSeesCandidate |= targetableCollectorCandidate.enclosesCollector(reflectorRootCollector);

                    if (!reflectorRootCollectorSeesCandidate) {
                        // The targetable collector candidate isn't visible to the root of this tree.
                        continue targetableGemCandidateLoop;
                    }
                }
           
            // If there are any reflectors attached to the same subtree as reflectors for the retargeting collector gem,
            // the collector at the root of the tree must be able to access the definitions for those reflectors, as well
            // as the definition of the retargeting collector.
            // For subtrees rooted in collectors, the previous check is sufficient.
            // For subtrees not rooted in collectors, we check that there is a collector which can see both definitions.  ie.
            //   If there is a reflector in the subtree whose collector is enclosed by collectorGem (or is collectorGem),
            //     any reflector in the subtree whose collector is not enclosed by collectorGem must be a
            //     sibling of collectorGem or its ancestors (before and) after retargeting.
            //   If there is no reflector in the subtree whose collector is enclosed by collectorGem, there is nothing
            //     to worry about, since this means relative collector visibilities won't change as a result of the retargeting.
            //
            // For one collector to be visible to another, it must be the same collector, a sibling, an ancestor,
            //   a sibling of an ancestor, or a child
            // This can be simplified to: its target must enclose the other collector.
            // So, in order to be able to define a collector which can see all the reflector definitions in an unrooted subtree,
            //   the parents of the reflectors' collectors must form an ancestor (enclosement) chain.
            // Since we know that the collectors enclosed by the collector to retarget are not changed with respect to each other,
            //   we only have to check that the collector to which to retarget is part of the ancestor chain.
           
                for (final Set<CollectorGem> nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSet : nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSets) {
                    CollectorGem candidateTarget = targetableCollectorCandidate.getTargetCollectorGem();

                    Set<CollectorGem> setToCheck = new HashSet<CollectorGem>(nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSet);
                    if (candidateTarget != null) {
                        setToCheck.add(candidateTarget);
                    }

                    if (!GemGraph.formsAncestorChain(setToCheck)) {
                        continue targetableGemCandidateLoop;
                    }
                }

                // If we're here, the targetable gem candidate satisfies all the above constraints.
                targetableCollectors.add(targetableCollectorCandidate);
            }

        //
        // Now put the collectors in order.
        // The target gem always comes first.  Then all other collectors, sorted by name.
        //
       
        // If the gem graph target is targetable, note this and remove.
        CollectorGem gemGraphTarget = tableTop.getGemGraph().getTargetCollector();
        boolean hasGemGraphTarget = targetableCollectors.remove(gemGraphTarget);

        // Order all other collectors by name.
        CollectorGem[] collectorsArray = targetableCollectors.toArray(new CollectorGem[targetableCollectors.size()]);
        Arrays.sort(collectorsArray, new Comparator<CollectorGem>() {
            public int compare(CollectorGem o1, CollectorGem o2) {
                return o1.getUnqualifiedName().compareTo(o2.getUnqualifiedName());
            }

        });

        // Compose the list.
        List<CollectorGem> targetableCollectorsList = new ArrayList<CollectorGem>(collectorsArray.length + (hasGemGraphTarget ? 1 : 0));
        if (hasGemGraphTarget) {
            targetableCollectorsList.add(gemGraphTarget);
        }
        targetableCollectorsList.addAll(Arrays.asList(collectorsArray));

        return targetableCollectorsList;
    }

    /**
     * @param displayedGem the gem to show the popup menu for
     * @param forTableTop whether the menu is for the table top and should include table top specific items
     * @return the JPopupMenu to show for the given gem
     */
    JPopupMenu getGemPopupMenu(DisplayedGem displayedGem, boolean forTableTop) {
       
        JPopupMenu gemPopupMenu = new JPopupMenu();

        gemPopupMenu.add(GemCutter.makeNewMenuItem(getRunGemAction(displayedGem)));
        gemPopupMenu.add(GemCutter.makeNewMenuItem(getDeleteGemsAction()));
        gemPopupMenu.addSeparator();
        gemPopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getCutAction()));
        gemPopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getCopyAction()));
        gemPopupMenu.add(getCopySpecialMenu());

        if (forTableTop) {
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getSelectSubTreeAction()));
            gemPopupMenu.addSeparator();
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getTidySelectionAction(displayedGem)));
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getTidySubTreeAction(displayedGem)));
        }
       
        Gem gem = displayedGem.getGem();
       
        if (gem instanceof FunctionalAgentGem) {
            gemPopupMenu.addSeparator();
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getViewPropertiesAction((FunctionalAgentGem)gem)));
           
        } else if (gem instanceof CodeGem) {
            gemPopupMenu.addSeparator();
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getRenameGemAction(gem)));
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getEditCodeGemAction((CodeGem)gem)));
       
        } else if (gem instanceof RecordFieldSelectionGem) {
            gemPopupMenu.addSeparator();
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getChangeRecordSelectionFieldAction(gem)));
           

        } else if (gem instanceof RecordCreationGem) {
            gemPopupMenu.addSeparator();

            // If a recordCreationGem is connected @ output, disable all menu items
            boolean shouldEnable = !gem.getOutputPart().isConnected();
           
            // ADD field menu item
            JMenuItem addRecordFieldMenu = GemCutter.makeNewMenuItem(getAddNewRecordFieldAction(gem));
            addRecordFieldMenu.setEnabled(shouldEnable);
            gemPopupMenu.add(addRecordFieldMenu);
           
            // get all the fields
            List<String> allFields = ((RecordCreationGem)gem).getCopyOfFieldsList();

            // DELETE field submenu
            final JMenu deleteRecordFieldMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_DeleteRecordField"));
            gemPopupMenu.add(deleteRecordFieldMenu);

            List<String> deletableFields = ((RecordCreationGem)gem).getDeletableFields(tableTop);
            deleteRecordFieldMenu.setEnabled(shouldEnable);

            // RENAME field submenu
            final JMenu renameRecordFieldMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_RenameRecordField"));
            gemPopupMenu.add(renameRecordFieldMenu);
           
            List<String> renamableFields = ((RecordCreationGem)gem).getRenamableFields(tableTop);
            renameRecordFieldMenu.setEnabled(shouldEnable);

            // Add the submenu items
            for (final String field : allFields) {
                // DELETABLE fields for submenu items, enable only if the field is deletable
                JMenuItem deleteMenuItem = GemCutter.makeNewMenuItem(getDeleteRecordFieldAction((RecordCreationGem)gem, field));

                deleteMenuItem.setEnabled(deletableFields.contains(field));
                deleteRecordFieldMenu.add(deleteMenuItem);

                // RENAMABLE fields for submenu items enable only if the field is renamable
                JMenuItem renameMenuItem = GemCutter.makeNewMenuItem(getRenameRecordFieldAction((RecordCreationGem)gem, field));
                renameMenuItem.setEnabled(renamableFields.contains(field));
                renameRecordFieldMenu.add(renameMenuItem);

            }

            // If disabled, set tool tips to indicate why
            if(!shouldEnable) {
                addRecordFieldMenu.setToolTipText(GemCutter.getResourceString("CannotModifyFields_tooltip", "add"));
                deleteRecordFieldMenu.setToolTipText(GemCutter.getResourceString("CannotModifyFields_tooltip", "delete"));
                renameRecordFieldMenu.setToolTipText(GemCutter.getResourceString("CannotModifyFields_tooltip", "rename"));
            }
           
        } else if (gem instanceof CollectorGem) {
            CollectorGem collectorGem = (CollectorGem)gem;
            CollectorGem targetCollector = collectorGem.getTargetCollectorGem();
           
            gemPopupMenu.addSeparator();
           
           
            // Create popup menu item for retargeting collectors.
            final JMenu retargetCollectorMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_RetargetCollector"));
            gemPopupMenu.add(retargetCollectorMenu);
           
            // Get the targetable collectors, enable the menu item if not empty.
            List<CollectorGem> targetableCollectors = getTargetableCollectors(collectorGem, null, null);
            retargetCollectorMenu.setEnabled(!targetableCollectors.isEmpty());
           
            // Create submenu items for retargeting to a collector.
            for (final CollectorGem targetableCollector : targetableCollectors) {
                // Make the new menu item.
                JMenuItem newMenuItem = GemCutter.makeNewMenuItem(getRetargetCollectorAction(collectorGem, targetableCollector));
               
                // Enable if its not already targeted to this collector.
                newMenuItem.setEnabled(targetableCollector != targetCollector);
               
                // Add the menu item.
                retargetCollectorMenu.add(newMenuItem);
            }
           
            // Create other menu items.
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getRenameGemAction(gem)));
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getAddReflectorAction(collectorGem)));
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getSaveGemAction(collectorGem)));
            gemPopupMenu.add(GemCutter.makeNewMenuItem(getEditPropertiesAction(collectorGem)));
        }
       
        return gemPopupMenu;
    }

    /**
     * @param part the part the action is for
     * @return the action to disconnect a connected gem part
     */   
    Action getDisconnectAction(final DisplayedPartConnectable part) {
       
        Action disconnectAction = new AbstractAction (GemCutter.getResourceString("PopItem_Disconnect")) {
           
            private static final long serialVersionUID = 2309882084879614674L;

            public void actionPerformed(ActionEvent evt) {
                Connection connection = part.getPartConnectable().getConnection();
               
                // Start the update now so that any auto-unburning is part of the same edit.
                tableTop.getUndoableEditSupport().beginUpdate();
               
                tableTop.handleDisconnectGesture(connection);
               
                if (part instanceof DisplayedPartOutput) {
                    tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(part.getGem());
                } else {
                    tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(connection.getSource().getGem());
                }

                tableTop.getUndoableEditSupport().setEditName(GemCutter.getResourceString("UndoText_DisconnectGems"));               
                tableTop.getUndoableEditSupport().endUpdate();
            }
        };
       
        disconnectAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_DISCONNECT));
       
        return disconnectAction;   
    }

    /**
     * @param part a displayed output or input part with the connection to be split
     * @return the action to split a connection into a collector / emitter pair
     */
    Action getSplitConnectionAction(final DisplayedPartConnectable part) {

        Action splitConnectionAction = new AbstractAction(GemCutter.getResourceString("PopItem_SplitConnection")) {

            private static final long serialVersionUID = 3698559866440806289L;

            public void actionPerformed(ActionEvent evt) {
                Connection connection = part.getPartConnectable().getConnection();
                tableTop.doSplitConnectionUserAction(connection);
            }
        };

        splitConnectionAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_SPLITCONNECTION));
        return splitConnectionAction;   
    }

   
    /**
     * @param partInput the part to connect a value gem to
     * @return a new Action for connection a ValueGem to the given part
     */
    Action getConnectValueGemAction(final DisplayedPartInput partInput) {

        Action connectAction = new AbstractAction(GemCutter.getResourceString("PopItem_ConnectValueGem"),
                new ImageIcon(getClass().getResource("/Resources/constant.gif"))) {
           
            private static final long serialVersionUID = 6931521030141991559L;

            public void actionPerformed(ActionEvent evt) {
                DisplayedGem dGem = tableTop.createDisplayedValueGem(new Point());
                ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();
               
                editSupport.beginUpdate();
                tableTop.doAddGemUserAction(dGem, partInput.getConnectionPoint());
                tidyAsConnected(dGem.getDisplayedOutputPart(), partInput, false);
               
                tableTop.handleConnectGemPartsGesture(dGem.getGem().getOutputPart(), partInput.getPartInput());

                editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
                editSupport.endUpdate();
            }                                             
        };

        // Check if a value gem can be connected here       
        DisplayedGem displayedValueGem = tableTop.createDisplayedValueGem(new Point());
        boolean connectable = GemGraph.isDefaultableValueGemSource(displayedValueGem.getDisplayedOutputPart().getPartOutput(), partInput.getPartInput(), gemCutter.getConnectionContext());
        connectAction.setEnabled(connectable);
       
        return connectAction;
    }

    /**
     * @param inputPart the input part whose argument is being retargeted.
     * @param targetableCollector the collector to which the argument is being retargeted.
     * @return a new Action to retarget the input's argument to the given collector.
     */
    Action getRetargetInputArgumentAction(final Gem.PartInput inputPart, final CollectorGem targetableCollector) {

        Action retargetInputArgumentAction = new AbstractAction(targetableCollector.getUnqualifiedName()) {
           
            private static final long serialVersionUID = 9119710886417993470L;

            public void actionPerformed(ActionEvent evt) {
                tableTop.handleRetargetInputArgumentGesture(inputPart, targetableCollector);
            }                                             
        };

        retargetInputArgumentAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_RETARGET_INPUT));
       
        return retargetInputArgumentAction;
    }
   
    /**
     * @param collectorGemToTarget the input part whose argument is being retargeted.
     * @param targetableCollector the collector to which the argument is being retargeted.
     * @return a new Action to retarget the input's argument to the given collector.
     */
    Action getRetargetCollectorAction(final CollectorGem collectorGemToTarget, final CollectorGem targetableCollector) {

        Action retargetInputArgumentAction = new AbstractAction(targetableCollector.getUnqualifiedName()) {
           
            private static final long serialVersionUID = -1932705179986107636L;

            public void actionPerformed(ActionEvent evt) {
                tableTop.handleRetargetCollectorGesture(collectorGemToTarget, targetableCollector);
            }                                             
        };

        retargetInputArgumentAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_RETARGET_COLLECTOR));
       
        return retargetInputArgumentAction;
    }
   
    /**
     * @param displayedPartOutput the part to connect a collector to
     * @return a new action for connecting a collector to the given part
     */
    Action getConnectCollectorAction(final DisplayedPartOutput displayedPartOutput) {

        Action connectAction = new AbstractAction(GemCutter.getResourceString("PopItem_ConnectCollector"),
                new ImageIcon(getClass().getResource("/Resources/collector.gif"))) {
           
            private static final long serialVersionUID = -7979201959532611095L;

            public void actionPerformed(ActionEvent evt) {
                // Calculate the target for the collector gem to create.
                // The friendliest thing to do is to minimize the depth of the collector, so this will be the
                //   parent of the deepest collector among reflectors in the subtree.
                int targetingCollectorMaxDepth = 1;     // depth of collectors targeting collectorToTarget.
                CollectorGem collectorToTarget = tableTop.getTargetCollector();

                Set<ReflectorGem> subtreeReflectors = UnsafeCast.<Set<ReflectorGem>>unsafeCast(GemGraph.obtainSubTreeGems(displayedPartOutput.getGem(), ReflectorGem.class));
               
                for (final ReflectorGem subtreeReflector : subtreeReflectors) {
                    CollectorGem subtreeReflectorCollector = subtreeReflector.getCollector();
                   
                    int subtreeReflectorCollectorDepth = GemGraph.getCollectorDepth(subtreeReflectorCollector);
                    if (subtreeReflectorCollectorDepth > targetingCollectorMaxDepth) {
                        targetingCollectorMaxDepth = subtreeReflectorCollectorDepth;
                        collectorToTarget = subtreeReflectorCollector.getTargetCollectorGem();
                    }
                }
               
                // Now actually create, add, and connect the collector.
                DisplayedGem dGem = tableTop.createDisplayedCollectorGem(new Point(0, 0), collectorToTarget);
                ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();
               
                editSupport.beginUpdate();
                tableTop.doAddGemUserAction(dGem, displayedPartOutput.getConnectionPoint());
                tidyAsConnected(displayedPartOutput, dGem.getDisplayedInputPart(0), true);

                tableTop.handleConnectGemPartsGesture(displayedPartOutput.getPartOutput(), dGem.getGem().getInputPart(0));

                editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
                editSupport.endUpdate();
               
                tableTop.displayLetNameEditor((CollectorGem)dGem.getGem());
            }                                             
        };
       
        return connectAction;
    }
   
    /**
     * Tidy up two gems as though they were connected.
     * @param displayedPartOutput the output of one of the gems.
     * @param displayedPartInput the input of the other gem.
     * @param anchorOutput whether the output should be the anchor. 
     *   If false, the input will be the anchor for tidying (ie. will not move in the tidy operation).
     */
    private void tidyAsConnected(DisplayedPartOutput displayedPartOutput, DisplayedPartInput displayedPartInput, boolean anchorOutput) {

        // Save old connection info.
        DisplayedConnection oldOutputConnection = displayedPartOutput.getDisplayedConnection();
        DisplayedConnection oldInputConnection = displayedPartInput.getDisplayedConnection();

        // Create a temporary connection between the parts.
        DisplayedConnection tempConnection = new DisplayedConnection(displayedPartOutput, displayedPartInput);
        displayedPartInput.bindDisplayedConnection(tempConnection);
        displayedPartOutput.bindDisplayedConnection(tempConnection);
       
        // tidy the temporary connection..
        DisplayedGem[] displayedGems = {displayedPartInput.getDisplayedGem(), displayedPartOutput.getDisplayedGem()};
        Graph.LayoutArranger layoutArranger = new Graph.LayoutArranger(displayedGems);
        DisplayedPartConnectable anchorPart = anchorOutput ? (DisplayedPartConnectable)displayedPartOutput : displayedPartInput;
        tableTop.doTidyUserAction(layoutArranger, anchorPart.getDisplayedGem());
       
        // Restore the old connection info.
        displayedPartOutput.bindDisplayedConnection(oldOutputConnection);
        displayedPartInput.bindDisplayedConnection(oldInputConnection);
    }
   
    /**
     * Returns the action to burn a gem part.
     * @param part the gem part to burn
     * @return Action
     */
    Action getBurnAction(final DisplayedPartInput part) {

        Action burnAction = new AbstractAction (GemCutter.getResourceString("PopItem_Burn"),
                new ImageIcon(getClass().getResource("/Resources/burnMenuIcon.gif"))) {
           
            private static final long serialVersionUID = -4017522251797979086L;

            public void actionPerformed(ActionEvent evt) {
                tableTop.getBurnManager().handleBurnInputGesture(part);
            }
        };
       
        burnAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_BURN));
       
        return burnAction;       
    }

    /**
     * Returns the action to unburn a gem part.
     * @param part the gem part to burn
     * @return Action
     */
    Action getUnburnAction(final DisplayedPartInput part) {

        Action unburnAction = new AbstractAction (GemCutter.getResourceString("PopItem_Unburn"),
                new ImageIcon(getClass().getResource("/Resources/unburnMenuIcon.gif"))) {
           
            private static final long serialVersionUID = -1510333509310634733L;

            public void actionPerformed(ActionEvent evt) {
                tableTop.getBurnManager().handleBurnInputGesture(part);
            }
        };

        unburnAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_UNBURN));
       
        return unburnAction;       
    }
   
    /**
     * Returns the action that adds a value gem.
     * @return Action
     */
    private Action getAddValueGemAction() {
       
        Action addValueGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddValueGem"),
                new ImageIcon(getClass().getResource("/Resources/constant.gif"))) {
           
            private static final long serialVersionUID = 6775526511235880852L;

            public void actionPerformed(ActionEvent evt) {
                TableTop tableTop = gemCutter.getTableTop();
                DisplayedGem dGem = tableTop.createDisplayedValueGem(currentPopupLocation);
                ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();
               
                editSupport.beginUpdate();
                tableTop.doAddGemUserAction(dGem, currentPopupLocation);
                editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
                editSupport.endUpdate();
            }
        };
       
        addValueGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_VALUE_GEM));
       
        return addValueGemAction;
    }

    /**
     * Returns the action that adds a new gem (aka Intellicut).
     * @return Action
     */
    private Action getAddGemAction() {
       
        Action addGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddGem"),
                new ImageIcon(getClass().getResource("/Resources/addNewGem.gif"))) {

            private static final long serialVersionUID = 2943954914796413469L;

            public void actionPerformed(ActionEvent evt) {
                gemCutter.getIntellicutManager().startIntellicutModeForTableTop(new Rectangle(currentPopupLocation), currentPopupLocation);
            }
        };
       
        addGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_INTELLICUT));
        addGemAction.putValue(Action.ACCELERATOR_KEY, GemCutterActionKeys.ACCELERATOR_INTELLICUT);
       
        return addGemAction;
    }

    /**
     * Returns the action that adds a code gem.
     * @return Action
     */
    private Action getAddCodeGemAction() {
       
        Action addCodeGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddCodeGem"),
                new ImageIcon(getClass().getResource("/Resources/code.gif"))) {
           
            private static final long serialVersionUID = 9129526613956299918L;

            public void actionPerformed(ActionEvent evt) {
                TableTop tableTop = gemCutter.getTableTop();
                DisplayedGem dGem = tableTop.createDisplayedCodeGem(currentPopupLocation);
                ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();
               
                editSupport.beginUpdate();
                tableTop.doAddGemUserAction(dGem, currentPopupLocation);
                editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
                editSupport.endUpdate();
            }
        };
       
        addCodeGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_CODE_GEM));
       
        return addCodeGemAction;
    }

    /**
     * Returns the action that adds a new collector.
     * @return Action
     */
    private Action getAddCollectorAction() {
       
        Action addCollectorAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddCollector"),
                new ImageIcon(getClass().getResource("/Resources/collector.gif"))) {
           
            private static final long serialVersionUID = 6261574181526925026L;

            public void actionPerformed(ActionEvent evt) {
                TableTop tableTop = gemCutter.getTableTop();
                DisplayedGem dGem = tableTop.createDisplayedCollectorGem(currentPopupLocation, tableTop.getTargetCollector());
                ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();
               
                editSupport.beginUpdate();
                tableTop.doAddGemUserAction(dGem, currentPopupLocation);
                editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
                editSupport.endUpdate();
               
                tableTop.displayLetNameEditor((CollectorGem)dGem.getGem());
            }
        };
       
        addCollectorAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_COLLECTOR_GEM));
       
        return addCollectorAction;
    }
   
    /**
     * Returns the action that adds a RecordFieldSelection gem.
     * @return Action
     */
    private Action getAddRecordFieldSelectionGemAction() {
       
        Action AddRecordFieldSelectionGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddRecordFieldSelectionGem"),
                new ImageIcon(getClass().getResource("/Resources/recordFieldSelectionGem.gif"))) {
           
            private static final long serialVersionUID = -642626553800845519L;

            public void actionPerformed(ActionEvent evt) {
                TableTop tableTop = gemCutter.getTableTop();
                DisplayedGem dGem = tableTop.createDisplayedRecordFieldSelectionGem(currentPopupLocation);
                ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();
               
                editSupport.beginUpdate();
                tableTop.doAddGemUserAction(dGem, currentPopupLocation);
                editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
                editSupport.endUpdate();
            }
        };
       
        AddRecordFieldSelectionGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_RECORD_FIELD_SELECTION_GEM));

        return AddRecordFieldSelectionGemAction;
    }

    /**
     * Returns the action that adds a new RecordCreationGem
     * @return Action
     */   
    private Action getAddRecordCreationGemAction() {

        Action AddRecordCreationGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddRecordCreationGem"),
                new ImageIcon(getClass().getResource("/Resources/recordCreationGem.gif"))) {

            private static final long serialVersionUID = -1527976012193744771L;

            public void actionPerformed(ActionEvent evt) {
                TableTop tableTop = gemCutter.getTableTop();
                DisplayedGem dGem = tableTop.createDisplayedRecordCreationGem(currentPopupLocation);
                ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();

                editSupport.beginUpdate();
                tableTop.doAddGemUserAction(dGem, currentPopupLocation);
                editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
                editSupport.endUpdate();
            }
        };

        AddRecordCreationGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_RECORD_CREATION_GEM));

        return AddRecordCreationGemAction;
    }
   
   
    /**
     * Returns the action that adds a new reflector.
     * @return Action
     */
    private Action getAddReflectorAction() {
       
        Action addReflectorAction = new AbstractAction(GemCutter.getResourceString("PopItem_AddReflector"),
                new ImageIcon(getClass().getResource("/Resources/reflector.gif"))) {
           
            private static final long serialVersionUID = -1527976012193744771L;

            public void actionPerformed(ActionEvent evt) {
                TableTop tableTop = gemCutter.getTableTop();
                DisplayedGem dGem = tableTop.createDisplayedReflectorGem(currentPopupLocation, gemCutter.getCollectorForAddingReflector());
                ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport();
               
                editSupport.beginUpdate();
                tableTop.doAddGemUserAction(dGem, currentPopupLocation);
                editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText()));
                editSupport.endUpdate();
            }
        };
       
        addReflectorAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_REFLECTOR_GEM));
       
        return addReflectorAction;
    }
   
    /**
     * Returns the action that tidies the table top.
     * @return Action
     */
    private Action getTidyTableTopAction() {
       
        Action tidyTableTopAction = new AbstractAction (GemCutter.getResourceString("ArrangeGraph")) {
           
            private static final long serialVersionUID = -5117623881659632367L;

            public void actionPerformed(ActionEvent evt) {
               
                tableTop.doTidyTableTopAction();
                repaint ();
            }
        };
       
        tidyTableTopAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ARRANGE_GRAPH));
        tidyTableTopAction.putValue(Action.ACCELERATOR_KEY, GemCutterActionKeys.ACCELERATOR_ARRANGE_GRAPH);
       
        return tidyTableTopAction;
    }
   
    /**
     * Returns the action that fits the table top.
     * @return Action
     */
    private Action getFitTableTopAction() {
       
        Action fitTableTopAction = new AbstractAction (GemCutter.getResourceString("FitTableTop")) {
           
            private static final long serialVersionUID = 8996494637943723837L;

            public void actionPerformed(ActionEvent evt) {
               
                tableTop.doShrinkTableTopUserAction();
                repaint();
            }
        };
       
        fitTableTopAction.putValue(Action.ACCELERATOR_KEY, GemCutterActionKeys.ACCELERATOR_FIT_TABLETOP);
        fitTableTopAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_FIT_TABLETOP));
       
        return fitTableTopAction;
    }   
   
    /**
     * Returns the Intellicut popup menu action.
     * @return Action
     */
    private Action getIntellicutAction(final DisplayedPartConnectable part) {
       
        Action intellicutAction = new AbstractAction(GemCutter.getResourceString("PopItem_Intellicut"),
                new ImageIcon(getClass().getResource("/Resources/intellicut.gif"))) {
           
            private static final long serialVersionUID = 3589489782252599609L;

            public void actionPerformed(ActionEvent e) {
               
                if (part != null) {
                    tableTop.maybeStartIntellicutMode(part);
                } else {
                    tableTop.getIntellicutManager().startIntellicutModeForTableTop(new Rectangle(currentPopupLocation), currentPopupLocation);
                }
            }
        };
       
        boolean enabled = true;
       
        if (part != null) {

            boolean burnt = false;
            if (part instanceof DisplayedPartInput) {
                burnt = ((DisplayedPartInput) part).getPartInput().isBurnt();
            }
           
            enabled = part.getPartConnectable().getType() != null && !part.isConnected() && !burnt;
        }
       
        intellicutAction.setEnabled(enabled);
        intellicutAction.putValue(Action.ACCELERATOR_KEY, GemCutterActionKeys.ACCELERATOR_INTELLICUT);
        intellicutAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_INTELLICUT));
       
        return intellicutAction;
    }
   
    /**
     * @return the paste gems action
     */
    private Action getPasteGemsAction() {
       
        Action action = new AbstractAction(GemCutter.getResourceString("Paste"),
                new ImageIcon(GemCutter.class.getResource("/Resources/paste.gif"))) {

            private static final long serialVersionUID = -4338435545421989629L;

            public void actionPerformed(ActionEvent e) {
                pasteLocation = currentPopupLocation;
                gemCutter.pasteFromClipboard();
                pasteLocation = null;
            }
        };
       
        action.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_PASTE));
        action.setEnabled(gemCutter.getPasteAction().isEnabled());
       
        return action;
    }
   
    /**
     * @param gem the gem the action is for
     * @return the run gem action
     */
    private Action getRunGemAction(final DisplayedGem gem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_RunGem"),
                new ImageIcon(GemCutter.class.getResource("/Resources/play.gif"))) {

            private static final long serialVersionUID = 8017728923330766355L;

            public void actionPerformed(ActionEvent e) {
                gemCutter.runTarget(gem);
            }
        };
       
        action.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_RUN));
        action.setEnabled(gem.getGem().isRunnable());
       
        return action;
    }

    /**
     * @return the delete gems action
     */
    private Action getDeleteGemsAction() {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_DeleteGem")) {

            private static final long serialVersionUID = 6190720497415948604L;

            public void actionPerformed(ActionEvent e) {
                Set<Gem> selectedGems = new HashSet<Gem>(Arrays.asList(tableTop.getSelectedGems()));
                tableTop.handleDeleteGemsGesture(selectedGems);
            }
        };
       
        action.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_DELETE));
       
        Gem[] selectedGems = tableTop.getSelectedGems();
        if (selectedGems.length > 1) {
            action.putValue(Action.NAME, GemCutter.getResourceString("PopItem_DeleteGems"));
        } else {
            action.putValue(Action.NAME, GemCutter.getResourceString("PopItem_DeleteGem"));

            boolean selectedTargetOnly = selectedGems.length != 0 && selectedGems[0] == tableTop.getTargetCollector();
            action.setEnabled(!selectedTargetOnly);
        }
       
        return action;
    }
   
    /**
     * @return the select gem subtree action
     */
    private Action getSelectSubTreeAction() {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_SelectSubTree")) {

            private static final long serialVersionUID = 5395741198879471469L;

            public void actionPerformed(ActionEvent e) {
                tableTop.doSelectSubtreeUser(tableTop.getSelectedGems());
            }
        };
       
        return action;
    }
   
    /**
     * @param gem the gem the action is for
     * @return the tidy selection action
     */
    private Action getTidySelectionAction(final DisplayedGem gem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_TidySelection")) {

            private static final long serialVersionUID = -8127271684299167786L;

            public void actionPerformed(ActionEvent e) {
                tableTop.doTidyUserAction(new Graph.LayoutArranger(tableTop.getSelectedDisplayedGems()), gem);
            }
        };
       
        action.setEnabled(tableTop.getSelectedDisplayedGems().length > 1);
       
        return action;
    }

    /**
     * @param gem the gem the action is for
     * @return the tidy sub trees action
     */
    private Action getTidySubTreeAction(final DisplayedGem gem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_TidySubtrees")) {

            private static final long serialVersionUID = 7701234830348136274L;

            public void actionPerformed(ActionEvent e) {
                Gem[] gems = tableTop.getSelectedGems();
                List<DisplayedGem> displayedGems = tableTop.doSelectSubtreeUser(gems);
                DisplayedGem[] validNodes = new DisplayedGem[displayedGems.size()];
                tableTop.doTidyUserAction(new Graph.LayoutArranger(displayedGems.toArray(validNodes)), gem);
            }
        };
       
        // Only enable the action if an input is actually connected.
        boolean enabled = false;
       
        Gem.PartInput[] inputs = gem.getGem().getInputParts();
        for (final PartInput input : inputs) {
            if (input.isConnected()) {
                enabled = true;
                break;
            }
        }

        action.setEnabled(enabled);
       
        return action;
    }

    /**
     * @param collectorGem the gem the action is for
     * @return the save gem action
     */
    private Action getSaveGemAction(final CollectorGem collectorGem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_SaveGem"),
                new ImageIcon(GemCutter.class.getResource("/Resources/save.gif"))) {

            private static final long serialVersionUID = -5967804767355281406L;

            public void actionPerformed(ActionEvent e) {
                gemCutter.saveGem();
            }
        };
       
        action.setEnabled(collectorGem.isRunnable());
       
        return action;
    }
   
    /**
     * @param gem the gem the action is for
     * @return the rename gem action
     */
    private Action getRenameGemAction(final Gem gem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_RenameGem")) {

            private static final long serialVersionUID = -1805921578643387361L;

            public void actionPerformed(ActionEvent e) {
                if (gem instanceof CodeGem) {
                    tableTop.displayCodeNameEditor((CodeGem)gem);
                } else if (gem instanceof CollectorGem) {
                    tableTop.displayLetNameEditor((CollectorGem)gem);
                } else {
                    throw new IllegalArgumentException("can only rename code or collector gems");
                }
            }
        };
       
        return action;
    }
   
    /**
     * Gets the action for changing the record field to be extracted.
     * @param gem the gem the action is for
     * @return the change extracted field action
     */
    private Action getChangeRecordSelectionFieldAction(final Gem gem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_ChangeSelectedField")) {
           
            private static final long serialVersionUID = 2446643701742147405L;

            public void actionPerformed(ActionEvent e) {
                if (gem instanceof RecordFieldSelectionGem) {
                    tableTop.displayRecordFieldSelectionEditor((RecordFieldSelectionGem)gem);
                } else {
                    throw new IllegalArgumentException("can only change extracted field on RecordFieldSelection gems");
                }
            }
        };
       
        if (gem instanceof RecordFieldSelectionGem) {
            //prevent editing of the gem when the tree is broken as it could lead to a more inconsistent state
            if ( GemGraph.isAncestorOfBrokenGemForest(gem.getRootGem())) {
                action.setEnabled(false);
            }
        }
       
        return action;
    }
   
   
    /**
     * Gets the action for renaming a record field
     * @param gem the gem the action is for
     * @return the renaming field action
     */
    private Action getRenameRecordFieldAction(final Gem gem, final String fieldToRename) {

        Action action = new AbstractAction(fieldToRename.toString()) {
            private static final long serialVersionUID = 806578875701283074L;

            public void actionPerformed(ActionEvent e) {
                if (gem instanceof RecordCreationGem) {
                    tableTop.displayFieldRenameEditor((RecordCreationGem)gem, FieldName.make(fieldToRename));
                } else {
                    throw new IllegalArgumentException("Can only rename field name on a RecordCreationGem");
                }
            }
        };

        if (gem instanceof RecordCreationGem) {
            //prevent editing of the gem when the output is connected
            if (gem.getOutputPart().isConnected()) {
                action.setEnabled(false);
               
            }
        }
       
        return action;
    }
   
 
    /**
     * Gets the action for adding a new record field
     * @param gem the gem the action is for
     * @return the adding new record field action
     */
    private Action getAddNewRecordFieldAction(final Gem gem) {

        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_AddNewRecordField")) {
            private static final long serialVersionUID = -1676013303676395830L;

            public void actionPerformed(ActionEvent e) {
                if (gem instanceof RecordCreationGem) {
                    tableTop.doAddRecordFieldUserAction((RecordCreationGem)gem);

                } else {
                    throw new IllegalArgumentException("can only add new field on RecordCreation gems");
                }
            }
        };
       
        return action;
    }
   
   
    /**
     * Gets the action for deleting an existing record field
     * @param gem the gem the action is for
     * @param fieldToDelete the field to be deleted
     * @return the deleting existing field action
     */
    private Action getDeleteRecordFieldAction(final Gem gem, final String fieldToDelete) {

        Action action = new AbstractAction(fieldToDelete.toString()) {
            private static final long serialVersionUID = -959693970346246044L;

            public void actionPerformed(ActionEvent e) {
                if (gem instanceof RecordCreationGem) {
                    tableTop.doDeleteRecordFieldUserAction((RecordCreationGem)gem, fieldToDelete);
                }
            }
        };

        return action;
    }
   
   
   
    /**
     * @param collectorGem the collector gem the action is for
     * @return the add reflector gem action
     */
    private Action getAddReflectorAction(final CollectorGem collectorGem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_AddReflector"),
                new ImageIcon(GemCutter.class.getResource("/Resources/reflector.gif"))) {

            private static final long serialVersionUID = 6212576237546675112L;

            public void actionPerformed(ActionEvent e) {
                DisplayedGem eGem = tableTop.createDisplayedReflectorGem(new Point(10, 10), collectorGem);
                gemCutter.setAddingDisplayedGem(eGem);
                gemCutter.enterGUIState(GemCutter.GUIState.ADD_GEM);
            }
        };
       
        return action;
    }
   
    /**
     * @param collectorGem the collector gem the action is for
     * @return the edit properties action
     */
    private Action getEditPropertiesAction(final CollectorGem collectorGem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_EditGemProperties"),
                new ImageIcon(GemCutter.class.getResource("/Resources/nav_edit.gif"))) {

            private static final long serialVersionUID = -7376813996995048934L;

            public void actionPerformed(ActionEvent e) {
                gemCutter.getNavigatorOwner().editMetadata(collectorGem);
            }
        };
       
        action.setEnabled(collectorGem.isConnected());
       
        return action;
    }

    /**
     * @param faGem the functional agent gem the action is for
     * @return the view properties action
     */
    private Action getViewPropertiesAction(final FunctionalAgentGem faGem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_ViewGemProperties")) {

            private static final long serialVersionUID = 945262478582661871L;

            public void actionPerformed(ActionEvent e) {
                GemEntity gemEntity = faGem.getGemEntity();
                gemCutter.getNavigatorOwner().displayMetadata(gemEntity, true);
            }
        };
       
        return action;
    }
   
    /**
     * @param codeGem the code gem the action is for
     * @return the edit code gem action
     */
    private Action getEditCodeGemAction(final CodeGem codeGem) {
       
        Action action = new AbstractAction(GemCutter.getResourceString("PopItem_OpenCodeEditor"),
                new ImageIcon(GemCutter.class.getResource("/Resources/selectedCodeEditorOpen.gif"))) {

            private static final long serialVersionUID = 1866715362157650129L;

            public void actionPerformed(ActionEvent e) {
                tableTop.showCodeGemEditor(codeGem, !tableTop.isCodeEditorVisible(codeGem));
            }
        };
       
        if (tableTop.isCodeEditorVisible(codeGem)) {
            action.putValue(Action.NAME, GemCutter.getResourceString("PopItem_CloseCodeEditor"));
        } else {
            action.putValue(Action.NAME, GemCutter.getResourceString("PopItem_OpenCodeEditor"));
        }
       
        return action;
    }       
   
    /**
     * This will display the intellicut menu by the currently selected gem
     * or if no gem is selected in the top-left of the table top. This is called
     * by the GemCutter if the user presses the intellicut keyboard shortcut.
     */
    void displayIntellicut() {
       
        DisplayedGem target = tableTop.getFocusedDisplayedGem();
        Rectangle menuLocation = null;
       
        if (target == null) {
            Rectangle visible = getVisibleRect();
            menuLocation = new Rectangle(visible.x + 10, visible.y + 10);
        } else {
            menuLocation = target.getBounds();
        }
       
        tableTop.getIntellicutManager().startIntellicutModeForTableTop(menuLocation);
    }
   
    /**
     * Get the tooltip text when over the TableTop.
     * @return the tooltip text to display
     * @param mouseEvent where we are now
     */
    public String getToolTipText(MouseEvent mouseEvent) {
        // The toolTip to return (default is no tool tip).
        String toolTip = null;
       
        // Where are we now?
        Point where = mouseEvent.getPoint();
       
        // Check for tooltip hotspots  
        DisplayedPart displayedPart = tableTop.getGemPartUnder(where);

        // The naming policy to use for tooltips
        ScopedEntityNamingPolicy namingPolicy;
        ModuleTypeInfo currentModuleTypeInfo = tableTop.getCurrentModuleTypeInfo();
        if (currentModuleTypeInfo == null) {
            namingPolicy = ScopedEntityNamingPolicy.FULLY_QUALIFIED;
        } else {
            namingPolicy = new ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous(currentModuleTypeInfo);
        }

        // If it is an input or an output, then we deal with it here.
        if (displayedPart != null && displayedPart instanceof DisplayedPartConnectable) {
            PartConnectable part = ((DisplayedPartConnectable)displayedPart).getPartConnectable();
            toolTip = ToolTipHelpers.getPartToolTip(part, tableTop.getGemGraph(), namingPolicy, this);
           
        } else if (displayedPart instanceof DisplayedPartBody) {
           
            Gem gem = ((DisplayedPartBody) displayedPart).getGem();
           
            if (gem instanceof FunctionalAgentGem) {
                toolTip = ToolTipHelpers.getFunctionalAgentToolTip((FunctionalAgentGem) gem, this, GemCutter.getLocaleFromPreferences());

            } else if (gem instanceof CollectorGem) {
                CollectorGem collectorGem = (CollectorGem)gem;
                CollectorGem targetGem = collectorGem.getTargetCollectorGem();
                String targetGemString = (targetGem == null) ? GemCutterMessages.getString("NullCollectorTarget") : targetGem.getUnqualifiedName();
               
                StringBuilder text = new StringBuilder("<html>");
                text.append(GemCutterMessages.getString("CollectorTargetToolTip", targetGemString));
                text.append("<br>");
                text.append(GemCutterMessages.getString("ResultTypeToolTip", tableTop.getGemGraph().getTypeString(gem.getResultType(), namingPolicy)));
                text.append("</html>");
                toolTip = text.toString();

            } else if (gem instanceof ValueGem) {
                ValueEntryPanel vep = getValueEntryPanel((ValueGem)gem);
                Point vepPoint = SwingUtilities.convertPoint(this, where, vep);
                toolTip = vep.getToolTipText(vepPoint);
            }
        }
       
        return toolTip;
    }
   
    /**
     * returns the gempainter used to paint the gem graph
     * @return TableTopGemPainter
     */
    TableTopGemPainter getGemPainter() {
        return gemPainter;
    }
   
    /**
     * Returns the preferredSize of a JViewport whose view is this Scrollable
     * @return Dimension
     */
    public Dimension getPreferredScrollableViewportSize() {
        return getPreferredSize();
    }

    /**
     * Returns the "block" increment for scrolling in the specified direction.
     */
    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        return (orientation == SwingConstants.VERTICAL) ? visibleRect.height : visibleRect.width;
    }

    /**
     * Return true if a viewport should always force the height of this Scrollable to match the height of the viewport.
     * Currently, always returns false unless this scrollable's height is less than the Viewport's height.
     */
    public boolean getScrollableTracksViewportHeight() {
        if (getParent() instanceof JViewport) {
            return (((JViewport)getParent()).getHeight() > getPreferredSize().height);
        }
       
        return false;
    }

    /**
     * Return true if a viewport should always force the width of this Scrollable to match the width of the viewport.
     * Currently, always returns false unless this scrollable's width is less than the Viewport's width.
     */
    public boolean getScrollableTracksViewportWidth() {
        if (getParent() instanceof JViewport) {
            return (((JViewport)getParent()).getWidth() > getPreferredSize().width);
        }
       
        return false;
    }

    /**
     * Returns the "unit" increment for scrolling in the specified direction.
     */
    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        // TEMP: Gotta figure out an optimal unit increment.
        return 30;
    }
   
    /**
     * @return the location at which new gems should be pasted if paste was invoked form a popup menu
     * This will be null if paste was not invoked from a popup menu and gems should be pasted at a default
     * location.
     */
    Point getPasteLocation(){
        return pasteLocation;
    }
   
    /**
     * Return the rectangle formed by the two specified Gems.
     * The rectangle is defined by the centre points of the two Gems.
     * @param fromGem DisplayedGem - the Gem that is the origin of the rectangle
     * @param toGem DisplayedGem - the Gem that closes the rectangle
     * @return Rectangle
     */
    private static Rectangle2D getRectangleForDisplayedGems(DisplayedGem fromGem, DisplayedGem toGem) {
        Point2D fromGemCentrePoint = fromGem.getCenterPoint();
        Rectangle2D rect = new Rectangle2D.Double(fromGemCentrePoint.getX(), fromGemCentrePoint.getY(), 0, 0);
        rect.add(toGem.getCenterPoint());

        return rect;
    }

    /**
     * returns whether tabletop is in photolook.
     * @return boolean
     */
    boolean isPhotoLook() {
        return gemCutter.isPhotoLook();
    }
}
TOP

Related Classes of org.openquark.gems.client.TableTopPanel$KeyStrokeHandler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.
script>