tufts.vue.Actions.java Source code

Java tutorial

Introduction

Here is the source code for tufts.vue.Actions.java

Source

/*
* Copyright 2003-2010 Tufts University  Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 * 
 * http://www.osedu.org/licenses/ECL-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package tufts.vue;

import java.awt.Component;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.Clipboard;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;

import org.apache.commons.lang.ArrayUtils;

import tufts.Util;
import tufts.vue.LWComponent.ChildKind;
import tufts.vue.LWComponent.Flag;
import tufts.vue.LWComponent.HideCause;
import tufts.vue.NodeTool.NodeModeTool;
import tufts.vue.gui.DeleteSlideDialog;
import tufts.vue.gui.DockWindow;
import tufts.vue.gui.FullScreen;
import tufts.vue.gui.GUI;
import tufts.vue.gui.VueFileChooser;
import tufts.vue.gui.renderer.SearchResultTableModel;
import edu.tufts.vue.metadata.MetadataList;
import edu.tufts.vue.preferences.ui.PreferencesDialog;

/**
 * VUE actions, all subclassed from VueAction, of generally these types:
 *      - application actions (e.g., new map)
 *      - actions that work on the active viewer (e.g., zoom)
 *      - actions that work on the active map (e.g., undo, select all)
 *      - actions that work on the current selection (e.g., font size, delete)
 *        (These are LWCAction's)
 *
 * @author Scott Fraize
 * @version March 2004
 */

public class Actions implements VueConstants {
    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(Actions.class);

    public static final int COMMAND = VueUtil.isMacPlatform() ? Event.META_MASK : Event.CTRL_MASK;
    public static final int LEFT_OF_SPACE = VueUtil.isMacPlatform() ? Event.META_MASK : Event.ALT_MASK;
    public static final int CTRL = Event.CTRL_MASK;
    public static final int SHIFT = Event.SHIFT_MASK;
    public static final int ALT = Event.ALT_MASK;

    public static final int CTRL_ALT = VueUtil.isMacPlatform() ? CTRL + COMMAND : CTRL + ALT;

    public static final String MENU_INDENT = "  ";

    static final private KeyStroke keyStroke(int vk, int mod) {
        return KeyStroke.getKeyStroke(vk, mod);
    }

    static final private KeyStroke keyStroke(int vk) {
        return keyStroke(vk, 0);
    }

    private static final String local(String resourceKey) {
        return VueResources.local(resourceKey);
    }

    //--------------------------------------------------
    // PDF Export Notes Actions
    //--------------------------------------------------

    public static final VueAction NodeNotesOutline = new VueAction(VueResources.local("menu.file.nodenotes")) {
        public void act() {
            File pdfFile = getFileForActiveMap("Node_Outline");
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createNodeOutline(pdfFile);
        }
    };

    private static final File getFileForPresentation(String type) {
        if (VUE.getActivePathway() == null || VUE.getActivePathway().getEntries().isEmpty()) {
            VueUtil.alert(null, VueResources.local("presentationNotes.invalidPresentation.message"),
                    VueResources.local("presentationNotes.invalidPathway.title"));
            return null;
        }
        VueFileChooser chooser = VueFileChooser.getVueFileChooser();

        File pdfFileName = null;
        chooser.setDialogTitle(VueResources.local("dialog.title.saveaspdf"));

        String baseName = VUE.getActivePathway().getLabel();
        if (baseName.indexOf(".") > 0)
            baseName = VUE.getActiveMap().getLabel().substring(0, baseName.lastIndexOf("."));
        baseName = baseName.replaceAll("\\*", "") + "_" + type;

        chooser.setSelectedFile(new File(baseName));
        int option = chooser.showSaveDialog(tufts.vue.VUE.getDialogParent());
        if (option == VueFileChooser.APPROVE_OPTION) {
            pdfFileName = chooser.getSelectedFile();

            if (pdfFileName == null)
                return null;

            if (!pdfFileName.getName().endsWith(".pdf"))
                pdfFileName = new File(pdfFileName.getAbsoluteFile() + ".pdf");

            if (pdfFileName.exists()) {
                int n = VueUtil.confirm(null,
                        VueResources.local("replaceFile.text") + " \'" + pdfFileName.getName() + "\'",
                        VueResources.local("replaceFile.title"), JOptionPane.YES_NO_OPTION,
                        JOptionPane.WARNING_MESSAGE);

            }

            return pdfFileName;
        } else
            return null;

    }

    private static final File getFileForActiveMap() {
        return getFileForActiveMap(null);
    }

    private static final File getFileForActiveMap(String type) {
        if (VUE.getActiveMap() == null) {
            VueUtil.alert(null, VueResources.local("dialog.activemap.message"),
                    VueResources.local("dialog.activemap.title"));
            return null;
        }
        VueFileChooser chooser = VueFileChooser.getVueFileChooser();
        File pdfFileName = null;

        String baseName = VUE.getActiveMap().getLabel();
        if (baseName.indexOf(".") > 0)
            baseName = VUE.getActiveMap().getLabel().substring(0, baseName.lastIndexOf("."));
        if (type != null)
            baseName = baseName.replaceAll("\\*", "") + "_" + type;
        else
            baseName = baseName.replaceAll("\\*", "");

        chooser.setSelectedFile(new File(baseName));
        chooser.setDialogTitle(VueResources.local("dialog.title.saveaspdf"));
        int option = chooser.showSaveDialog(tufts.vue.VUE.getDialogParent());
        if (option == VueFileChooser.APPROVE_OPTION) {
            pdfFileName = chooser.getSelectedFile();

            if (pdfFileName == null)
                return null;

            if (!pdfFileName.getName().endsWith(".pdf"))
                pdfFileName = new File(pdfFileName.getAbsoluteFile() + ".pdf");

            if (pdfFileName.exists()) {
                int n = VueUtil.confirm(null,
                        VueResources.local("replaceFile.text") + " \'" + pdfFileName.getName() + "\'",
                        VueResources.local("replaceFile.title"), JOptionPane.YES_NO_OPTION,
                        JOptionPane.WARNING_MESSAGE);

            }

            return pdfFileName;
        } else
            return null;

    }

    /*     
    public static final VueAction ZoteroAction =
     new VueAction("Import Zotero collection") {
    public void act() 
    {         
     VueFileChooser chooser = VueFileChooser.getVueFileChooser();
     File zoteroFile = null;
                              
       int option = chooser.showOpenDialog(tufts.vue.VUE.getDialogParent());
       if (option == VueFileChooser.APPROVE_OPTION) 
       {
           zoteroFile = chooser.getSelectedFile();
              
        edu.tufts.vue.zotero.ZoteroAction.importZotero(zoteroFile);
       }
        
    }
    };
    */
    public static final VueAction SpeakerNotes1 = new VueAction(
            VueResources.local("menu.file.exporthandout.speakernotes1")) {
        public void act() {
            File pdfFile = getFileForPresentation("Speaker notes");
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createSpeakerNotes1PerPage(pdfFile);
        }
    };
    public static final VueAction SpeakerNotes4 = new VueAction(
            VueResources.local("menu.file.exporthandout.speakernotes4")) {
        public void act() {
            File pdfFile = getFileForPresentation("Speaker notes");
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createSpeakerNotes4PerPage(pdfFile);
        }
    };
    public static final VueAction NodeNotes4 = new VueAction(
            VueResources.local("menu.file.exporthandout.nodenotes4")) {
        public void act() {
            File pdfFile = getFileForActiveMap("Node_Notes");
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createNodeNotes4PerPage(pdfFile);
        }
    };
    public static final VueAction SpeakerNotesOutline = new VueAction(
            VueResources.local("menu.file.exporthandout.speakernotesoutline")) {
        public void act() {
            File pdfFile = getFileForPresentation("Speaker notes");
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createOutline(pdfFile);
        }
    };
    public static final VueAction Slides8PerPage = new VueAction(
            VueResources.local("menu.file.exporthandout.slides8perpage")) {
        public void act() {
            File pdfFile = getFileForPresentation("Slides");
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createPresentationNotes8PerPage(pdfFile);
        }
    };

    public static final VueAction AudienceNotes = new VueAction(
            VueResources.local("menu.file.exporthandout.audiencenotes")) {
        public void act() {
            File pdfFile = getFileForPresentation("Audience notes");
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createAudienceNotes(pdfFile);
        }
    };

    public static final VueAction FullPageSlideNotes = new VueAction(
            VueResources.local("menu.file.exporthandout.fullpageslidenotes")) {
        public void act() {
            File pdfFile = getFileForPresentation("Slides");
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createPresentationSlidesDeck(pdfFile);
        }
    };

    public static final VueAction MapAsPDF = new VueAction(VueResources.local("menu.file.exporthandout.mapaspdf")) {
        public void act() {
            File pdfFile = getFileForActiveMap();
            if (pdfFile != null)
                tufts.vue.PresentationNotes.createMapAsPDF(pdfFile);
        }
    };

    //------------------------------------------------------------
    // Preference Action
    //------------------------------------------------------------
    public static final class PreferenceAction extends VueAction {
        private PreferencesDialog dialog = null;

        public PreferenceAction() {
            super(VueResources.local("menu.edit.preferences"), keyStroke(KeyEvent.VK_COMMA, COMMAND));
        }

        public void act() {
            if (dialog == null) {
                Log.info("creating new preference dialog");
                dialog = new PreferencesDialog(null, VueResources.local("menu.edit.preferences"),
                        edu.tufts.vue.preferences.PreferencesManager.class, true, null, false);
            }
            dialog.setVisible(true);
        }
    };

    public static final VueAction Preferences = new PreferenceAction();
    //-------------------------------------------------------
    // Selection actions
    //-------------------------------------------------------

    public static final Action SelectAll = new VueAction(VueResources.local("menu.edit.selectall"),
            keyStroke(KeyEvent.VK_A, COMMAND)) {
        public void act() {
            selection().setTo(focal().getAllDescendents(ChildKind.EDITABLE));
        }
    };

    public static final Action SelectAllLinks = new VueAction(VueResources.local("menu.edit.selectlink")) {
        public void act() {
            selection().setTo(focal().getDescendentsOfType(ChildKind.EDITABLE, LWLink.class));
        }
    };

    public static final Action SelectAllNodes = new VueAction(VueResources.local("menu.edit.selectnodes")) {
        public void act() {
            selection().setTo(focal().getDescendentsOfType(ChildKind.EDITABLE, LWNode.class));
        }
    };

    public static final Action DeselectAll = new LWCAction(VueResources.local("menu.edit.deselectall"),
            keyStroke(KeyEvent.VK_A, SHIFT + COMMAND)) {
        boolean enabledFor(LWSelection s) {
            return s.size() > 0;
        }

        public void act() {
            selection().clear();
        }
    };

    public static final Action Reselect = new VueAction(VueResources.local("menu.edit.reselect"),
            keyStroke(KeyEvent.VK_R, COMMAND)) {
        public void act() {
            selection().reselect();
        }
    };

    public static final LWCAction ExpandSelection = new LWCAction(VueResources.local("menu.edit.expandselection"),
            keyStroke(KeyEvent.VK_SLASH, COMMAND)) {
        public void act() {
            VUE.getInteractionToolsPanel().doExpand();
        }

        boolean enabledFor(LWSelection s) {
            return s.size() > 0 && VUE.getInteractionToolsPanel().canExpand();
        }
    };

    public static final LWCAction ShrinkSelection = new LWCAction(VueResources.local("menu.edit.shrinkselection"),
            keyStroke(KeyEvent.VK_PERIOD, COMMAND)) {
        public void act() {
            VUE.getInteractionToolsPanel().doShrink();
        }

        boolean enabledFor(LWSelection s) {
            return s.size() > 0 && VUE.getInteractionToolsPanel().canShrink();
        }
    };

    public static final Action AddPathwayItem = new LWCAction(VueResources.local("actions.addPathwayItem.label")) {
        @Override
        public void act(LWSelection s) {
            LWPathway pathway = VUE.getActivePathway();
            if (!pathway.isOpen())
                pathway.setOpen(true);
            LWComponent[] sorted = s.asArray();
            java.util.Arrays.sort(sorted, LWComponent.GridSorter);
            VUE.getActivePathway().add(Util.asList(sorted).iterator());
            GUI.makeVisibleOnScreen(VUE.getActiveViewer(), PathwayPanel.class);
        }

        //         public void act(Iterator i) {
        //            LWPathway pathway = VUE.getActivePathway();
        //            if (!pathway.isOpen())
        //               pathway.setOpen(true);
        //             VUE.getActivePathway().add(i);
        //             GUI.makeVisibleOnScreen(VUE.getActiveViewer(), PathwayPanel.class);
        //         }
        boolean enabledFor(LWSelection s) {
            // items can be added to pathway as many times as you want
            return VUE.getActivePathway() != null && s.size() > 0;
        }
    };

    public static final Action RemovePathwayItem = new LWCAction(
            VueResources.local("actions.removePathwayItem.label")) {
        public void act(Iterator i) {
            VUE.getActivePathway().remove(i);
        }

        boolean enabledFor(LWSelection s) {
            LWPathway p = VUE.getActivePathway();
            return p != null && s.size() > 0 && (s.size() > 1 || p.contains(s.first()));
        }
    };

    public static final Action AddResource = new VueAction(VueResources.local("action.addresource")) {
        public void act() {

            DataSourceViewer.getAddLibraryAction().actionPerformed(null);
            GUI.makeVisibleOnScreen(this, VUE.getContentDock().getClass());
            VUE.getContentPanel().showResourcesTab();
        }
    };

    public static final Action UpdateResource = new VueAction(VueResources.local("action.updateresource")) {
        public void act() {

            DataSourceViewer.getUpdateLibraryAction().actionPerformed(null);
            GUI.makeVisibleOnScreen(this, VUE.getContentDock().getClass());
            VUE.getContentPanel().showResourcesTab();
        }
    };

    public static final Action SearchFilterAction = new VueAction(VueResources.local("action.search")) {

        public void act() {
            VUE.getMetadataSearchMainGUI().setVisible(true);

            //                 if(tufts.vue.ui.InspectorPane.META_VERSION == tufts.vue.ui.InspectorPane.OLD)
            //                 {    
            //                  VUE.getMapInfoDock().setVisible(true);
            //                  VUE.getMapInspectorPanel().activateFilterTab();
            //                 }
            //                 else
            //                 {
            // //                  tufts.vue.gui.DockWindow searchWindow = tufts.vue.MetadataSearchMainGUI.getDockWindow();
            // //                  searchWindow.setVisible(true);
            //                    VUE.getMetadataSearchMainGUI().setVisible(true);                     
            //                 }
        }
    };

    //-------------------------------------------------------
    // Alternative View actions
    //-------------------------------------------------------

    /**Addition by Daisuke Fujiwara*/

    public static final Action HierarchyView = new LWCAction(VueResources.local("action.hierarchyview")) {
        public void act(LWNode n) {
            LWNode rootNode = n;
            String name = new String(rootNode.getLabel() + "'s Hierarchy View");
            String description = new String("Hierarchy view model of " + rootNode.getLabel());

            LWHierarchyMap hierarchyMap = new LWHierarchyMap(name);

            tufts.oki.hierarchy.HierarchyViewHierarchyModel model = new tufts.oki.hierarchy.HierarchyViewHierarchyModel(
                    rootNode, hierarchyMap, name, description);

            hierarchyMap.setHierarchyModel(model);
            hierarchyMap.addAllComponents();
            VUE.displayMap((LWMap) hierarchyMap);
        }

        boolean enabledFor(LWSelection s) {
            return s.size() == 1 && s.first() instanceof LWNode;
        }
    };

    /**End of Addition by Daisuke Fujiwara*/

    public static final Action PreviewInViewer = new LWCAction(VueResources.local("action.inviewer")) {
        public void act(Iterator i) {
            GUI.makeVisibleOnScreen(VUE.getActiveViewer(), tufts.vue.ui.SlideViewer.class);
        }

        boolean enabledFor(LWSelection s) {
            return s.size() == 1 && s.first() instanceof LWSlide;
        }
    };

    public static final Action MasterSlide = new VueAction(VueResources.local("action.masterslide")) {
        public void act() {
            if (VUE.getSlideDock() != null) {
                VUE.getSlideDock().setVisible(true);
                VUE.getSlideViewer().showMasterSlideMode();
            }
        }
    };
    public static final Action PreviewOnMap = new LWCAction(VueResources.local("menu.pathways.editslide")) {
        public void act(LWComponent c) {

            final MapViewer viewer = VUE.getActiveViewer();

            if (viewer.getFocal() == c) {
                viewer.popFocal(true, true);
                return;
                //return false;
            }

            final Rectangle2D viewerBounds = viewer.getVisibleMapBounds();
            final Rectangle2D mapBounds = c.getMapBounds();
            final Rectangle2D overlap = viewerBounds.createIntersection(mapBounds);
            final double overlapArea = overlap.getWidth() * overlap.getHeight();
            //final double viewerArea = viewerBounds.getWidth() * viewerBounds.getHeight();
            final double nodeArea = mapBounds.getWidth() * mapBounds.getHeight();
            final boolean clipped = overlapArea < nodeArea;

            final double overlapWidth = mapBounds.getWidth() / viewerBounds.getWidth();
            final double overlapHeight = mapBounds.getHeight() / viewerBounds.getHeight();

            final boolean focusNode; // otherwise, re-focus map      

            if (clipped) {
                focusNode = true;
            } else if (overlapWidth > 0.8 || overlapHeight > 0.8) {
                focusNode = false;
            } else
                focusNode = true;

            boolean AnimateOnZoom = false;

            if (focusNode) {
                viewer.clearRollover();

                if (true) {
                    // loadfocal animate only currently works when popping (to a parent focal)
                    //viewer.loadFocal(this, true, AnimateOnZoom);
                    ZoomTool.setZoomFitRegion(viewer, mapBounds, 0, AnimateOnZoom);
                    viewer.loadFocal(c);
                } else {
                    ZoomTool.setZoomFitRegion(viewer, mapBounds, -LWPathway.PathBorderStrokeWidth / 2,
                            AnimateOnZoom);
                }
            } else {
                // just re-fit to the map
                viewer.fitToFocal(AnimateOnZoom);
            }

        }

        boolean enabledFor(LWComponent s) {
            return s instanceof LWSlide;
        }
    };

    public static void startPresentation(final LWPathway pathway, final Object source) {
        VUE.setActive(LWPathway.class, source, pathway);

        // TODO: we should be able to start the pre-cache from
        // PresentationTool.startPresentation(), but currently the map does a
        // full-repaint on the full-screen viewer before we load the new focal for the
        // first item in the presentation.  We should do this in such a way that the
        // entire map does NOT paint on the full-screen viewer before the presentation
        // starts.

        if (pathway != null && !Images.lowMemoryConditions()) {
            // If running really low on memory, this might make a presentation worse.
            pathway.preCacheContent();
        }

        final PresentationTool presTool = PresentationTool.getTool();

        // We ideally want to do this first, so we don't seen a full-screen paint of the entire map,
        // but it's causing some problem when the presentation exits, where it leaves the viewer
        // at the last focal, instead of back out to the map.        
        //         GUI.invokeAfterAWT(new Runnable() { public void run() {
        //             presTool.startPresentation();
        //         }});

        final LWSelection savedSelection = VUE.getSelection().clone();
        // activating the presentation tool is going to clear the selection,
        // so we need to save it here and then pass it to startPresentation.

        GUI.invokeAfterAWT(new Runnable() {
            public void run() {
                VUE.toggleFullScreen(true);
            }
        });
        GUI.invokeAfterAWT(new Runnable() {
            public void run() {
                //VueToolbarController.getController().setSelectedTool(presTool);
                VUE.setActive(VueTool.class, source, presTool);
            }
        });
        GUI.invokeAfterAWT(new Runnable() {
            public void run() {
                presTool.startPresentation(savedSelection);
            }
        });
    }

    public static final VueAction LaunchPresentation = new VueAction(VueResources.local("action.preview")) {
        @Override
        public void act() {
            startPresentation(VUE.getActivePathway(), this);
        }

        @Override
        public boolean overrideIgnoreAllActions() {
            return true;
        }

    };

    public static final Action DeleteSlide = new VueAction(VueResources.local("action.delete")) {
        public void act() {
            //delete the current entry
            //                 This is a heuristic to try and best guess what the user might want to
            // actually remove.  If nothing in selection, and we have a current pathway
            // index/element, remove that current pathway element.  If one item in
            // selection, also remove whatever the current element is (which ideally is
            // usually also the selection, but if it's different, we want to prioritize
            // the current element hilighted in the PathwayTable).  If there's MORE than
            // one item in selection, do a removeAll of everything in the selection.
            // This removes ALL instances of everything in selection, so that, for
            // instance, a SelectAll followed by pathway delete is guaranteed to empty
            // the pathway entirely.

            LWPathway pathway = VUE.getActivePathway();

            if (pathway.getCurrentIndex() >= 0 && VUE.ModelSelection.size() < 2) {
                DeleteSlideDialog dsd = PathwayPanel.getDeleteSlideDialog();
                java.awt.Point p = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().getCenterPoint();
                p.x -= dsd.getWidth() / 2;
                p.y -= dsd.getHeight() / 2;
                dsd.setLocation(p);
                if (dsd.showAgain()) {
                    dsd.setVisible(true);
                }

                if (dsd.getOkCanel())
                    pathway.remove(pathway.getCurrentIndex());
            } else {
                DeleteSlideDialog dsd = PathwayPanel.getDeleteSlideDialog();
                java.awt.Point p = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().getCenterPoint();
                p.x -= dsd.getWidth() / 2;
                p.y -= dsd.getHeight() / 2;
                dsd.setLocation(p);
                if (dsd.showAgain()) {
                    dsd.setVisible(true);
                }

                if (dsd.getOkCanel())
                    pathway.remove(VUE.getSelection().iterator());
            }

        }
    };
    //-----------------------------------------------------------------------------
    // Link actions
    //-----------------------------------------------------------------------------

    public static final LWCAction LinkMakeStraight =
            //new LWCAction("Straight", VueResources.getIcon("linkTool.line.raw")) {
            new LWCAction(VueResources.local("menu.format.link.straight"),
                    VueResources.getIcon("link.style.straight")) {
                void init() {
                    putValue("property.value", new Integer(0));
                } // for use in a MenuButton

                boolean enabledFor(LWSelection s) {
                    if (!s.containsType(LWLink.class))
                        return false;
                    return s.size() == 1 ? ((LWLink) s.first()).getControlCount() != 0 : true;
                }

                public void act(LWLink c) {
                    c.setControlCount(0);
                }
            };
    public static final LWCAction LinkMakeQuadCurved =
            //new LWCAction("Curved", VueResources.getIcon("linkTool.curve1.raw")) {
            new LWCAction(VueResources.local("menu.format.link.curved"),
                    VueResources.getIcon("link.style.curved")) {
                void init() {
                    putValue("property.value", new Integer(1));
                }

                boolean enabledFor(LWSelection s) {
                    if (!s.containsType(LWLink.class))
                        return false;
                    return s.size() == 1 ? ((LWLink) s.first()).getControlCount() != 1 : true;
                }

                public void act(LWLink c) {
                    c.setControlCount(1);
                }
            };
    public static final LWCAction LinkMakeCubicCurved =
            //new LWCAction("S-Curved", VueResources.getIcon("linkTool.curve2.raw")) {
            new LWCAction(VueResources.local("menu.format.link.scurved"),
                    VueResources.getIcon("link.style.s-curved")) {
                void init() {
                    putValue("property.value", new Integer(2));
                }

                boolean enabledFor(LWSelection s) {
                    if (!s.containsType(LWLink.class))
                        return false;
                    return s.size() == 1 ? ((LWLink) s.first()).getControlCount() != 2 : true;
                }

                public void act(LWLink c) {
                    c.setControlCount(2);
                }
            };
    public static final Action LinkArrows = new LWCAction(VueResources.local("menu.format.link.arrow"),
            keyStroke(KeyEvent.VK_L, COMMAND)/*, VueResources.getIcon("outlineIcon.link")*/) {
        boolean enabledFor(LWSelection s) {
            return s.containsType(LWLink.class);
        }

        public void act(LWLink c) {
            c.rotateArrowState();
        }
    };

    public static final Action ResizeNode = new LWCAction(
            VueResources.local("menu.format.node.resize")/*, VueResources.getIcon("outlineIcon.link")*/) {
        boolean enabledFor(LWSelection s) {

            if (s.size() == 1 && s.containsType(LWNode.class)) {
                LWNode n = (LWNode) s.get(0);
                Size minSize = n.getMinimumSize();

                if (minSize.height == n.height && minSize.width == n.width)
                    return false;
                else
                    return true;

            } else
                return false;
        }

        public void act(LWNode c) {
            c.setToNaturalSize();
        }
    };

    /** Helper for menu creation.  Null's indicate good places
     * for menu separators. */
    public static final Action[] LINK_MENU_ACTIONS = { LinkMakeStraight, LinkMakeQuadCurved, LinkMakeCubicCurved,
            LinkArrows };

    //-----------------------------------------------------------------------------
    // Node actions
    //-----------------------------------------------------------------------------

    public static final LWCAction NodeMakeAutoSized = new LWCAction(VueResources.local("action.setautosized")) {
        boolean enabledFor(LWSelection s) {
            if (!s.containsType(LWNode.class))
                return false;
            return s.size() == 1 ? ((LWNode) s.first()).isAutoSized() == false : true;
        }

        public void act(LWNode c) {
            c.setAutoSized(true);
        }
    };

    /** Helper for menu creation.  Null's indicate good places
     * for menu separators. */
    public static final Action[] NODE_MENU_ACTIONS = { NodeMakeAutoSized };

    //-------------------------------------------------------
    // Edit actions: Duplicate, Cut, Copy & Paste
    // These actions all make use of the statics
    // below.
    //-------------------------------------------------------

    private static final List<LWComponent> ScratchBuffer = new ArrayList();
    private static LWComponent StyleBuffer; // this holds the style copied by "Copy Style"

    private static final LWComponent.CopyContext CopyContext = new LWComponent.CopyContext(
            new LWComponent.LinkPatcher(), true);
    private static final List<LWComponent> DupeList = new ArrayList(); // cache for dupe'd items

    private static final int CopyOffset = 10;

    private static final boolean RECORD_OLD_PARENT = true; // not a flag: a constant for readability
    private static final boolean SORT_BY_Z_ORDER = true; // not a flag: a constant for readability

    public static List<LWComponent> duplicatePreservingLinks(Collection<LWComponent> items) {
        return duplicatePreservingLinks(items, !RECORD_OLD_PARENT, !SORT_BY_Z_ORDER);
    }

    /**
     * @param recordOldParent - if true, the old parent will be stored in the copy as a clientProperty
     * @param sortByZOrder - if true, will maintain the relative z-order of components in the dupe-set
     *
     * note: preserving old parents has different effect when duplicating the ScratchBuffer -- it will copy over old-parent client data
     * that was collected when the ScratchBuffer was loaded
     */
    public static List<LWComponent> duplicatePreservingLinks(final Collection<LWComponent> items,
            boolean recordOldParent, final boolean sortByZOrder) {
        CopyContext.reset();
        DupeList.clear();

        final Collection<LWComponent> ordered;
        if (sortByZOrder && items.size() > 1)
            ordered = Arrays.asList(LWContainer.sort(items, LWContainer.ZOrderSorter));
        else
            ordered = items;

        // todo: ideally to preserve z-order layering of duplicated
        // elements, probably merge LinkPatcher into CopyContext, and
        // while at it change dupe action to add all the children to
        // the new parent with a single addChildren event.

        for (LWComponent c : ordered) {

            // TODO: all users of this method may not be depending on items being
            // selected!  CopyContext should sort out duped items with a HashSet, only
            // invoking duplicate on items in set who don't have an ancestor in the set.
            // Can we use a TreeSet to track & preserve z-order?  When duping a whole
            // node w/children, z-order is already preserved -- it's only the order
            // amongst the top-level selected items we need to preserve (e.g., don't
            // just rely on the random selection order, unless we want to change the
            // selection to a SortedSet (TreeSet)).

            if (c.isAncestorSelected() || !canEdit(c)) {

                // Duplicate is hierarchical action: don't dupe if parent is going to do
                // it for us.  Note that when we call this on paste, parent will always
                // be null as these are orphans in the cut buffer, and thus
                // isAncestorSelected will always be false, but we culled them here when
                // we put them in, so we're all set.

                continue;
            }
            LWComponent copy = c.duplicate(CopyContext);
            if (recordOldParent) {
                // note check for the special instance of ScratchBuffer as the input Collection to this method:
                if (items == ScratchBuffer) {
                    // parent will be null -- copy over the client data we stored when loading the ScratchBuffer
                    copy.setClientData(LWKey.OLD_PARENT, c.getClientData(LWKey.OLD_PARENT));
                } else {
                    // we could store this as the actual parent, but if we do that,
                    // changes made to the components before they're fully baked
                    // (e.g., link reconnections, translations) will generate events
                    // that will confuse the UndoManager.
                    copy.setClientData(LWKey.OLD_PARENT, c.parent);
                }
                // Note: forcing the addition of client-data (a HashMap) on every
                // component during a duplicate/cut/paste just to store the old parent
                // is a bit expensive given how little we currently use
                // LWComponent.clientData.
            }
            if (copy != null)
                DupeList.add(copy);
            //System.out.println("duplicated " + copy);
        }
        CopyContext.complete();
        CopyContext.reset();
        return DupeList;
    }

    /** @return true if we can cut/copy/delete/duplicate this selection */
    private static boolean canEdit(LWSelection s) {
        if (s.size() == 1) {
            return canEdit(s.first());
        } else
            return s.size() > 1;

        //return s.size() > 0 && !(s.only() instanceof LWSlide);
    }

    private static boolean canEdit(LWComponent c) {
        if (c.isLocked())
            return false;
        else if (c.hasFlag(Flag.FIXED_LOCATION))
            return false;
        else if (c instanceof LWSlide && !DEBUG.META)
            return false;
        else if (c.getParent() instanceof LWPathway) // old-style slides not map-owned
            return false;
        else
            return true;
    }

    public static final LWCAction Duplicate = new LWCAction(VueResources.local("menu.edit.duplicate"),
            keyStroke(KeyEvent.VK_D, COMMAND)) {
        boolean mayModifySelection() {
            return true;
        }

        boolean enabledFor(LWSelection s) {
            return canEdit(s);
        }

        /**
         * this permits repeated duplicates: when a single duplicate is
         * made, it auto-activates label edit, which normally disables
         * all actions -- this way, we can duplicate again immediately.
         */
        @Override
        public boolean overrideIgnoreAllActions() {
            return true;
        }

        @Override
        void act(final LWSelection selection) {

            if (!haveViewer())
                return;

            //             if (viewer().hasActiveTextEdit() && selection().size() > 1) 
            //                 return;

            final List<LWComponent> dupes = duplicatePreservingLinks(selection, RECORD_OLD_PARENT, SORT_BY_Z_ORDER);

            final LWContainer parent0 = dupes.get(0).getClientData(LWKey.OLD_PARENT);
            boolean allHaveSameParent = true;

            // dupes may have fewer items in it that the selection: it will only
            // contain the top-level items duplicated -- not any of their children

            for (LWComponent copy : dupes) {
                if (copy.getClientData(LWKey.OLD_PARENT) != parent0)
                    allHaveSameParent = false;
                copy.translate(CopyOffset, CopyOffset);
            }

            //-----------------------------------------------------------------------------
            // Add the newly duplicated items to the appropriate new parent
            //-----------------------------------------------------------------------------

            if (allHaveSameParent) {
                parent0.addChildren(dupes, LWComponent.ADD_PASTE);
            } else {
                // Todo: would be smoother to collect all the nodes by parent
                // and do a separate collective adds for each parent
                for (LWComponent copy : dupes)
                    copy.getClientData(LWKey.OLD_PARENT).pasteChild(copy);

            }

            //-----------------------------------------------------------------------------

            // clear out old parent references now that we're done with them
            for (LWComponent copy : dupes) {
                //copy.flushAllClientData(); // start entirely fresh
                copy.setClientData(LWKey.OLD_PARENT, null);
            }

            //             if (selection.only() instanceof LWLink) {
            //                 LWLink oneLink = (LWLink) selection.first();
            //                 // if link is directed, and tail was connected,
            //                 // re-connect the tail -- duping another "outbound"
            //                 // link from a node.
            //             }

            selection().setTo(dupes);

            if (dupes.size() == 1 && dupes.get(0).supportsUserLabel())
                viewer().activateLabelEdit(dupes.get(0));
        }

    };

    public static final LWCAction Copy = new LWCAction(VueResources.local("menu.edit.copy"),
            keyStroke(KeyEvent.VK_C, COMMAND)) {
        boolean enabledFor(LWSelection s) {
            return canEdit(s);
        }

        void act(LWSelection selection) {
            ScratchBuffer.clear();
            // always record old parent when loading the ScratchBuffer -- the client data
            // that gets added never needs to be cleared, as client data isn't copied
            // when an individual LWComponent is duplicated, and once a LWComponent is in the
            // ScratchBuffer, that instance will never appear anywhere in a map -- it's only
            // used as a duplicating source.
            ScratchBuffer.addAll(duplicatePreservingLinks(selection, RECORD_OLD_PARENT, SORT_BY_Z_ORDER));

            // Enable if want to use system clipboard.  FYI: the clip board manager
            // will immediately grab all the data available from the transferrable
            // to cache in the system.
            //Clipboard clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard();
            //clipboard.setContents(VUE.getActiveViewer().getTransferableSelection(), null);

        }
    };

    public static final VueAction Paste = new VueAction(VueResources.local("menu.edit.paste"),
            keyStroke(KeyEvent.VK_V, COMMAND)) {
        //public boolean isEnabled() //would need to listen for scratch buffer fills

        private Point2D.Float lastMouseLocation;
        private Point2D.Float lastPasteLocation;

        public void act() {

            final MapViewer viewer = viewer();
            // note: preserving old parents has different effect when duplicating the ScratchBuffer -- it will copy over old-parent client data
            final List<LWComponent> pasted = duplicatePreservingLinks(ScratchBuffer, RECORD_OLD_PARENT,
                    !SORT_BY_Z_ORDER);
            final Point2D.Float mouseLocation = viewer.getLastFocalMousePoint();
            final Point2D.Float pasteLocation;

            if (mouseLocation.equals(lastMouseLocation) && lastPasteLocation != null) {
                pasteLocation = lastPasteLocation;
                // translate both current and last paste location:
                pasteLocation.x += CopyOffset;
                pasteLocation.y += CopyOffset;
            } else {
                pasteLocation = mouseLocation;
                lastPasteLocation = pasteLocation;
            }

            final LWComponent newParent = viewer.getDropFocal();

            if (newParent instanceof LWSlide) {
                // When pasting content from one slide to another slide, keep the relative x/y
                // position of the content from the old slide

                //final List<LWComponent> pasteToOldLocations = new ArrayList();
                final List<LWComponent> pasteToNewLocations = new ArrayList();
                for (LWComponent c : pasted) {
                    final LWContainer oldParent = c.getClientData(LWKey.OLD_PARENT);
                    if (oldParent instanceof LWSlide && oldParent != newParent) {
                        // if old parent was a slide (a different one), leave it's x/y alone when pasting
                        //pasteToOldLocations.add(c);
                        // don't actually need to record these
                    } else {
                        pasteToNewLocations.add(c);
                    }
                }
                if (pasteToNewLocations.size() > 0)
                    MapDropTarget.setCenterAt(pasteToNewLocations, pasteLocation);
            } else {
                MapDropTarget.setCenterAt(pasted, pasteLocation); // note: this method only works on un-parented nodes
            }

            newParent.addChildren(pasted, LWComponent.ADD_PASTE);

            for (LWComponent c : pasted) {
                //c.flushAllClientData(); // start entirely fresh
                c.setClientData(LWKey.OLD_PARENT, null); // clear out any old-parent client data
            }

            selection().setTo(pasted);
            lastMouseLocation = mouseLocation;
        }

        // stub code for if we want to start using the system clipboard for cut/paste
        void act_system() {
            Clipboard clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard();
            VUE.getActiveViewer().getMapDropTarget().processTransferable(clipboard.getContents(this), null);
        }

    };

    public static final Action Cut = new LWCAction(VueResources.local("menu.edit.cut"),
            keyStroke(KeyEvent.VK_X, COMMAND)) {
        boolean mayModifySelection() {
            return true;
        }

        boolean enabledFor(LWSelection s) {
            return canEdit(s);
        }

        void act(LWSelection selection) {
            Copy.act(selection);
            Delete.act(selection);
            //ScratchMap = null;  // okay to paste back in same location
        }
    };

    public static final LWCAction Delete =
            // "/tufts/vue/images/delete.png" looks greate (from jide), but too unlike others
            new LWCAction(VueResources.local("menu.edit.delete"), keyStroke(KeyEvent.VK_BACK_SPACE),
                    ":general/Delete") {
                //new LWCAction(VueResources.local("menu.edit.delete"), keyStroke(KeyEvent.VK_DELETE), ":general/Delete") {

                // OLD: [We could use BACK_SPACE instead of DELETE because that key is bigger, and
                // on the mac it's actually LABELED "delete", even tho it sends BACK_SPACE.
                // BUT, if we use backspace, trying to use it in a text field in, say the
                // object inspector panel causes it to delete the selection instead of
                // backing up a char...  The MapViewer special-case handles both anyway as a
                // backup.]

                // Update, SMF Oct 2009: BACK_SPACE is the default key-code when hitting "delete" on a
                // laptop, so it's better to use BACK_SPACE here.  Again, the MapViewer directly
                // handles both keys in special-case code so we don't need to worry about what happens
                // when the MapViewer has focus.  The issue with hitting the "delete" key in text fields
                // triggering the global Delete action appear to have gone away.  Changing this to
                // BACK_SPACE now is allowing the use of hitting "delete" after clicking on nodes in
                // the DataTree -- the unconsumed KeyPress is relayed through the FocusManager to the
                // VueMenuBar which then can correctly recognize the KeyPress as triggering the Delete
                // action, w/out the user having to press Fn-Delete, which is whats needed to actually
                // generate the VK_DELETE key code.

                boolean mayModifySelection() {
                    return true;
                }

                boolean enabledFor(LWSelection s) {
                    return canEdit(s);
                }

                void act(LWSelection s) {

                    s.clearAncestorSelected();

                    // the selection will now only contain the top levels in the
                    // the hierarchy of what's selected.

                    final Collection toDelete = new ArrayList();

                    for (LWComponent c : s) {
                        if (canEdit(c))
                            toDelete.add(c);
                        else
                            Log.info("delete not permitted: " + c);
                    }

                    for (LWContainer parent : s.getParents()) {
                        if (DEBUG.Enabled)
                            info("deleting for parent " + parent);
                        parent.deleteChildrenPermanently(toDelete);

                        // someday: would be nice if this could simply be
                        // handled as a traversal on the map: pass down
                        // the list of items to remove, and any parent
                        // that notices one of it's children removes it.

                    }

                    // LWSelection does NOT listen for events among what's selected (an
                    // optimization & we don't want the selection updating iself and issuing
                    // selection change events AS a delete takes place for each component as
                    // it's deleted) -- it only needs to know about deletions, so they're
                    // handled special case.  Here, all we need to do is clear the selection as
                    // we know everything in it has just been deleted.

                    selection().clear();

                }

                //             void act(LWSelection s) {
                //                 s.clearAncestorSelected();
                //                 act(s.iterator());
                //                 selection().clear();
                //             }

                //         void act(LWComponent c) {
                //             LWContainer parent = c.getParent();

                //             if (parent == null) {
                //                 info("skipping: null parent (already deleted): " + c);
                //             } else if (c.isDeleted()) {
                //                 info("skipping (already deleted): " + c);
                //             } else if (parent.isDeleted()) { // after prior check, this case should be impossible now
                //                 info("skipping (parent already deleted): " + c); // parent will call deleteChildPermanently
                //             } else if (parent.isSelected()) { // if parent selected, it will delete it's children
                //                 info("skipping - parent selected & will be deleting: " + c);
                //             } else if (c.isLocked()) {
                //                 info("not permitted: " + c);
                //             } else if (!canEdit(c)) {
                //                 info("cannot edit: " + c);
                //             } else {
                //                 parent.deleteChildPermanently(c);
                //             }
                //         }
            };

    public static final LWCAction CopyStyle = new LWCAction(VueResources.local("menu.format.copystyle"),
            keyStroke(KeyEvent.VK_C, COMMAND + SHIFT)) {
        boolean enabledFor(LWSelection s) {
            return s.size() == 1;
        }

        void act(LWComponent c) {
            try {
                StyleBuffer = c.getClass().newInstance();
            } catch (Throwable t) {
                tufts.Util.printStackTrace(t);
            }
            StyleBuffer.setLabel("styleHolder");
            StyleBuffer.copyStyle(c);
        }
    };

    public static final LWCAction PasteStyle = new LWCAction(VueResources.local("menu.format.applystyle"),
            keyStroke(KeyEvent.VK_V, COMMAND + SHIFT)) {
        boolean enabledFor(LWSelection s) {
            return s.size() > 0 && StyleBuffer != null;
        }

        void act(LWComponent c) {
            c.copyStyle(StyleBuffer);
        }
    };

    //-----------------------
    // Context Menu Actions
    //-----------------------
    public static final Action KeywordAction = new KeywordActionClass(
            MENU_INDENT + VueResources.local("mapViewer.componentMenu.keywords.label"));
    public static final Action ContextKeywordAction = new KeywordActionClass(
            VueResources.local("actions.addkeywords"));

    public static class KeywordActionClass extends VueAction {

        public KeywordActionClass(String s) {
            super(s);
        }

        public void act() {
            VUE.getInfoDock().setVisible(true);
            VUE.getInspectorPane().showKeywordView();
            GUI.makeVisibleOnScreen(this, tufts.vue.ui.InspectorPane.class);
            VUE.getInfoDock().setRolledUp(false, true);
        }
        //public void act() { VUE.ObjectInspector.setVisible(true); }
    };
    /*
    public static final LWCAction AddImageAction = new LWCAction(VueResources.local("mapViewer.componentMenu.addImage.label")) {
    public void act(LWComponent c) 
    {
       VueFileChooser chooser = new VueFileChooser();
      File fileName = null;
        
            // TODO: this is broken -- it should do almost exactly the same thing
            // as AddFileAction -- the only difference would when adding a new item
            // entirely, create an image instead of a node (and perhaps use
            // an image selecting file filter)
          
        int option = chooser.showOpenDialog(tufts.vue.VUE.getDialogParent());
        if (option == VueFileChooser.APPROVE_OPTION) 
        {
            fileName = chooser.getSelectedFile();
        
            if (fileName == null) 
               return;
        
            //if(!pdfFileName.getName().endsWith(".pdf")) 
            //   pdfFileName = new File(pdfFileName.getAbsoluteFile()+".pdf");                   
                
            //if (pdfFileName.exists()) {
             //   int n = JOptionPane.showConfirmDialog(null, VueResources.local("replaceFile.text") + " \'" + pdfFileName.getName() + "\'", 
             //           VueResources.local("replaceFile.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
             //                         
            //}
            //LWNode node = NodeModeTool.createNewNode();
            final LWImage image = new LWImage();
            image.setResource(fileName);
            //node.addChild(image);
            c.addChild(image);
        }
           
    }
    };
    */

    public static final LWCAction AddFileAction = new LWCAction(
            VueResources.local("mapViewer.componentMenu.addFile.label")) {
        public void act(LWComponent c) {
            VueFileChooser chooser = VueFileChooser.getVueFileChooser();

            File fileName = null;

            int option = chooser.showOpenDialog(tufts.vue.VUE.getDialogParent());
            if (option == VueFileChooser.APPROVE_OPTION) {
                fileName = chooser.getSelectedFile();

                if (fileName == null)
                    return;

                if (fileName.exists())
                    VueUtil.setCurrentDirectoryPath(chooser.getSelectedFile().getParent());

                if (c instanceof LWNode || c instanceof LWLink) {
                    //Resource r = c.getResource();

                    VUE.setActive(LWComponent.class, this, null);

                    c.setResource(fileName);
                    VUE.setActive(LWComponent.class, this, c);
                } else if (c instanceof LWSlide) {
                    VUE.setActive(LWComponent.class, this, null);

                    String f = (fileName.getName()).toLowerCase();
                    String extension = f.substring(f.lastIndexOf(".") + 1, f.length());
                    LWComponent node = null;
                    //System.out.println("STRING : " + extension);
                    if (extension.equals("jpg") || extension.equals("jpeg") || extension.equals("png")
                            || extension.equals("gif"))
                        node = new LWImage();
                    else {
                        node = NodeModeTool.createNewNode();
                        ((LWNode) node).setAsTextNode(true);
                    }

                    Resource resource = c.getResourceFactory().get(fileName);
                    node.setAutoSized(false);
                    node.setLabel(resource.getTitle());

                    node.setResource(resource);
                    VUE.getActiveViewer().getDropFocal().pasteChild(node);
                    VUE.getActiveViewer().getSelection().setTo(node);
                    VUE.setActive(LWComponent.class, this, c);

                }

                //  }
                /*  else                 
                  {
                     final Object[] defaultOrderButtons = { "Replace","Add","Cancel"};
                      int response = JOptionPane.showOptionDialog
                      ((Component)VUE.getApplicationFrame(),
                       new String("Do you want to replace the current resource or add this resource as a child node?"),
                       "Replace Resource?",
                       JOptionPane.YES_NO_CANCEL_OPTION,
                       JOptionPane.PLAIN_MESSAGE,
                       null,
                       defaultOrderButtons,             
                       "Add"
                       );                  
                          
                       
                      if (response == JOptionPane.YES_OPTION) { // Save
                  //c.setResource(new URLResource(fileName.getAbsolutePath()));
                  c.setResource(fileName);
                      } 
                      else if (response == JOptionPane.NO_OPTION) { // Don't Save
                  {
                      //LWNode node = NodeModeTool.createNewNode();
                          
                      Resource resource = c.getResourceFactory().get(fileName);
                      LWNode node= new LWNode(resource.getTitle());
                      node.setResource(resource);
                      //node.addChild(image);                         
                      c.addChild(node);
                  }
                      } else // anything else (Cancel or dialog window closed)
                  return;
                  }*/
            }
        }
    };

    public static final VueAction SaveCopyToZotero = new VueAction(VueResources.getString("zotero.saveCopy")) {

        public void act() {
            if (VUE.askSaveIfModified(VUE.getActiveMap())) {
                netscape.javascript.JSObject win = (netscape.javascript.JSObject) netscape.javascript.JSObject
                        .getWindow(VueApplet.getInstance());
                String[] arguments = { VUE.getActiveMap().getFile().getAbsolutePath(),
                        VUE.getActiveMap().getDisplayLabel() };
                win.call("doImportMap", arguments);
                // System.out.println("JS CALLED");
            }
        }
    };

    public static final LWCAction AddResourceToZotero = new LWCAction(VueResources.local("zotero.addResource")) {
        public void act(LWComponent c) {
            Resource r = c.getResource();
            if (r != null) {
                String spec = r.getSpec();

                if (spec.startsWith("http") || spec.startsWith("https")) {
                    //import from url
                    netscape.javascript.JSObject win = (netscape.javascript.JSObject) netscape.javascript.JSObject
                            .getWindow(VueApplet.getInstance());
                    String[] arguments = { spec };
                    win.call("doImportUrl", arguments);

                } else {
                    //import from file..
                    netscape.javascript.JSObject win = (netscape.javascript.JSObject) netscape.javascript.JSObject
                            .getWindow(VueApplet.getInstance());
                    String[] arguments = { spec, r.getTitle() };
                    win.call("doImportFile", arguments);

                }
            }
        }
    };
    public static final LWCAction AddURLAction = new LWCAction(
            VueResources.local("mapViewer.componentMenu.addURL.label")) {
        public void act(LWComponent c) {
            File fileName = null;
            String resourceString = "http://";
            Resource r = c.getResource();
            if (r != null)
                resourceString = r.getSpec();

            String title = VueResources
                    .local((c instanceof LWNode) ? "dialog.addurl.node.title" : "dialog.addurl.link.title");
            String option = (String) VueUtil.input(VUE.getApplicationFrame(),
                    VueResources.local("dialog.addurl.label"), title, JOptionPane.PLAIN_MESSAGE, null,
                    resourceString);

            if (option == null || option.length() <= 0)
                return;

            /*
             * At one point I was trying to do something clever if you tried to type a url with GET parameters
             * into the Add URL box but it seems to have caused more problems then it solved at this point.
             */
            /* if (option.indexOf("?") > 0)
            {
               String encoded = option.substring(option.indexOf("?")+1);
                   
               encoded = URLEncoder.encode(encoded);
                
               option = option.substring(0,option.indexOf("?")+1) + encoded;
            }*/
            //  if (!option.startsWith("http://") || !option.startsWith("https://") || !option.startsWith("file://"))
            //   option = "http://" + option;
            //int option = chooser.showOpenDialog(tufts.vue.VUE.getDialogParent());
            //if (option != null && option.length() > 0) {

            URI uri = null;

            try {
                uri = new URI(option);
            } catch (URISyntaxException e) {
                VueUtil.alert((Component) VUE.getApplicationFrame(),
                        VueResources.local("dialog.addurlaction.message"),
                        VueResources.local("dialog.addurlaction.title"), JOptionPane.ERROR_MESSAGE);
                return;
            }

            r = c.getResource();
            // if (r == null) {
            r = c.getResourceFactory().get(uri);
            if (r == null) {
                VueUtil.alert((Component) VUE.getApplicationFrame(),
                        VueResources.local("dialog.addurlaction.message"),
                        VueResources.local("dialog.addurlaction.title"), JOptionPane.ERROR_MESSAGE);
            } else {
                if (c instanceof LWNode || c instanceof LWLink) {
                    VUE.setActive(LWComponent.class, this, null);
                    c.setResource(r);
                    VUE.setActive(LWComponent.class, this, c);
                } else if (c instanceof LWSlide) {

                    VUE.setActive(LWComponent.class, this, null);
                    LWNode node = NodeModeTool.createNewNode();
                    Resource resource = c.getResourceFactory().get(uri);
                    //node.setStyle(c.getStyle());                    
                    //LWNode node= new LWNode(resource.getTitle());                  

                    //node.addChild(image);
                    VUE.getActiveViewer().getDropFocal().dropChild(node);

                    node.setLabel(uri.toString());
                    node.setResource(resource);
                    VUE.setActive(LWComponent.class, this, c);

                }

            }
            //                     try {
            //                         c.setResource(new URLResource(url.toURL()));
            //                     } catch (MalformedURLException e) {
            //                         JOptionPane.showMessageDialog((Component)VUE.getApplicationFrame(),
            //                                                       "Malformed URL, resource could not be added.", 
            //                                                       "Malformed URL", 
            //                                                       JOptionPane.ERROR_MESSAGE);
            //                     }  
            //}
            /*else {
                final Object[] defaultOrderButtons = { "Replace","Add","Cancel"};
                int response = JOptionPane.showOptionDialog
                    ((Component)VUE.getApplicationFrame(),
                     new String("Do you want to replace the current resource or add this resource as a child node?"),
                     "Replace Resource?",
                     JOptionPane.YES_NO_CANCEL_OPTION,
                     JOptionPane.PLAIN_MESSAGE,
                     null,
                     defaultOrderButtons,             
                     "Add"
                     );                  
                  
                
                if (response == JOptionPane.YES_OPTION) { // Save
                    r = c.getResourceFactory().get(uri);
                    if (r == null) {
                        JOptionPane.showMessageDialog((Component)VUE.getApplicationFrame(),
                                                      "Malformed URL, resource could not be added.", 
                                                      "Malformed URL", 
                                                      JOptionPane.ERROR_MESSAGE);
                    } else
                        c.setResource(r);
                        
            //                         try {
            //                             c.setResource(new URLResource(url.toURL()));
            //                         } catch (MalformedURLException e) {
            //                             JOptionPane.showMessageDialog((Component)VUE.getApplicationFrame(),
            //                                                           "Malformed URL, resource could not be added.", 
            //                                                           "Malformed URL", 
            //                                                           JOptionPane.ERROR_MESSAGE);
            //                             return;
            //                         }
                        
                } 
                else if (response == JOptionPane.NO_OPTION) { // Don't Save
                    //LWNode node = NodeModeTool.createNewNode();
                        
            //                             URLResource urlResource;
            //                             try {
            //                                 urlResource = new URLResource(url.toURL());
            //                             } catch (MalformedURLException e) {
            //                                 JOptionPane.showMessageDialog((Component)VUE.getApplicationFrame(),
            //                                                               "Malformed URL, resource could not be added.", 
            //                                                               "Malformed URL", 
            //                                                               JOptionPane.ERROR_MESSAGE);
            //                                 return;
            //                             }
            //                             URLResource urlResource;
                
                    r = c.getResourceFactory().get(uri);
                    if (r == null) {
                        JOptionPane.showMessageDialog((Component)VUE.getApplicationFrame(),
                                                      "Malformed URL, resource could not be added.", 
                                                      "Malformed URL", 
                                                      JOptionPane.ERROR_MESSAGE);
                    } else {
                        final LWNode node = new LWNode(uri.toString());
                        node.setResource(r);
                        //node.addChild(image);                         
                        c.addChild(node);
                    }
                } // else // anything else (Cancel or dialog window closed)
            } */
        }
    };

    public static final LWCAction EditMasterSlide = new LWCAction(
            VueResources.local("menu.pathways.editmasterslide")) {
        public void act(LWSlide slide) {
            final LWSlide masterSlide = slide.getMasterSlide();
            if (VUE.getActiveViewer() != null) {
                if (VUE.getActiveViewer().getFocal().equals(masterSlide)) {
                    VUE.getActiveViewer().loadFocal(VUE.getActiveMap());
                    VUE.setActive(LWMap.class, this, VUE.getActiveMap());
                    /*ZoomTool.setZoomFitRegion(VUE.getActiveViewer(),
                        zoomBounds,
                        0,
                        false);
                        */
                    //Point2D.Float originOffset = VUE.getActiveMap().getTempUserOrigin();
                    //   ZoomTool.setZoom(VUE.getActiveMap().getTempZoom());
                    //if (originOffset != null)
                    //VUE.getActiveViewer().setMapOriginOffset(originOffset.getX(), originOffset.getY());
                    ZoomTool.setZoomFitRegion(VUE.getActiveMap().getTempBounds());
                    VUE.getReturnToMapButton().setVisible(false);
                    VUE.depthSelectionControl.setVisible(true);
                } else {
                    if (!(VUE.getActiveViewer().getFocal() instanceof LWSlide)) {
                        //   zoomFactor = VUE.getActiveViewer().getZoomFactor();
                        //   VUE.getActiveMap().setTempZoom(VUE.getActiveViewer().getZoomFactor());                     
                        VUE.getActiveMap().setTempZoom(VUE.getActiveViewer().getZoomFactor());
                        VUE.getReturnToMapButton().setVisible(true);
                        VUE.depthSelectionControl.setVisible(false);
                        VUE.getActiveMap().setTempBounds(VUE.getActiveViewer().getVisibleMapBounds());
                        //   VUE.getActiveMap().setTempUserOrigin(VUE.getActiveViewer().getOriginLocation());
                    }
                    VUE.getActiveViewer().loadFocal(masterSlide);
                    // update inspectors (optional -- may not actually want to do this, but
                    // currently required if you want up/down arrows to subsequently navigate
                    // the pathway)

                    VUE.setActive(tufts.vue.MasterSlide.class, this, masterSlide);
                }

            }
            //           long now = System.currentTimeMillis();
            //           MapMouseEvent mme = new MapMouseEvent(new MouseEvent(VUE.getActiveViewer(),
            //                                                                      MouseEvent.MOUSE_CLICKED,
            //                                                                      now,
            //                                                                      5,5,5,5,
            //                                                                      false));
            //           ((LWSlide)c).getPathwayEntry().pathway.getMasterSlide().doZoomingDoubleClick(mme);
        }
    };

    //private static double zoomFactor =0;  
    //private static Point2D originOffset = null;

    public static class ReturnToMapAction extends VueAction {
        public void act() {
            final LWComponent focal = VUE.getActiveFocal();

            if (focal instanceof LWSlide || focal instanceof MasterSlide) {

                VUE.getActiveViewer().loadFocal(VUE.getActiveMap());
                VUE.setActive(LWMap.class, this, VUE.getActiveMap());

                /*ZoomTool.setZoomFitRegion(VUE.getActiveViewer(),
                  zoomBounds,
                  0,
                  false);
                */

                //Point2D.Float originOffset = VUE.getActiveMap().getTempUserOrigin();
                //double tempZoom = VUE.getActiveMap().getTempZoom();

                //System.out.println("temp #s : " +originOffset + " " + tempZoom);
                //   ZoomTool.setZoom(tempZoom);
                //if (originOffset != null)
                //VUE.getActiveViewer().setMapOriginOffset(originOffset.getX(), originOffset.getY());
                if (VUE.getActiveMap().getTempBounds() != null)
                    ZoomTool.setZoomFitRegion(VUE.getActiveMap().getTempBounds());
                VUE.getReturnToMapButton().setVisible(false);
                VUE.depthSelectionControl.setVisible(true);
                //ZoomTool.setZoom(zoomFactor);

            } else if (focal instanceof LWGroup) {
                VUE.getActiveViewer().loadFocal(VUE.getActiveMap());
                if (VUE.getActiveMap().getTempBounds() != null)
                    ZoomTool.setZoomFitRegion(VUE.getActiveMap().getTempBounds());
                VUE.getReturnToMapButton().setVisible(false);
                VUE.depthSelectionControl.setVisible(true);
            }
        }

        @Override
        public boolean overrideIgnoreAllActions() {
            return true;
        }
    }

    public static final VueAction ReturnToMap = new ReturnToMapAction();

    public static final LWCAction EditSlide = new LWCAction(VueResources.local("action.editslide")) {
        public void act(LWSlide slide) {
            //final LWSlide masterSlide = slide.getPathwayEntry().pathway.getMasterSlide();
            if (VUE.getActiveViewer() != null) {
                if (VUE.getActiveViewer().getFocal().equals(slide)
                        || VUE.getActiveViewer().getFocal() instanceof MasterSlide) {
                    VUE.getActiveViewer().loadFocal(VUE.getActiveMap());
                    VUE.setActive(LWMap.class, this, VUE.getActiveMap());
                    /*ZoomTool.setZoomFitRegion(VUE.getActiveViewer(),
                        zoomBounds,
                        0,
                        false);
                        */

                    //Point2D.Float originOffset = VUE.getActiveMap().getTempUserOrigin();
                    //double tempZoom = VUE.getActiveMap().getTempZoom();
                    //System.out.println("temp #s : " +originOffset + " " + tempZoom);
                    //ZoomTool.setZoom(tempZoom);
                    //if (originOffset != null)
                    //   VUE.getActiveViewer().setMapOriginOffset(originOffset.getX(), originOffset.getY());
                    if (VUE.getActiveMap().getTempBounds() != null)
                        ZoomTool.setZoomFitRegion(VUE.getActiveMap().getTempBounds());
                    VUE.getReturnToMapButton().setVisible(false);
                    VUE.depthSelectionControl.setVisible(true);
                    //ZoomTool.setZoom(zoomFactor);

                } else {

                    VUE.getActiveMap().setTempZoom(VUE.getActiveViewer().getZoomFactor());
                    VUE.getReturnToMapButton().setVisible(true);
                    VUE.depthSelectionControl.setVisible(false);
                    VUE.getActiveMap().setTempBounds(VUE.getActiveViewer().getVisibleMapBounds());
                    //VUE.getActiveMap().setTempUserOrigin(VUE.getActiveViewer().getOriginLocation());

                    //VUE.getActiveMap().setUserOrigin(p)
                    //VUE.getActiveViewer().getO
                    //originOffset = VUE.getActiveViewer().get
                    //zoomBounds = VUE.getActiveViewer().getDisplayableMapBounds();
                    //Point2D.Float originOffset = VUE.getActiveMap().getTempUserOrigin();
                    //double tempZoom = VUE.getActiveMap().getTempZoom();
                    //System.out.println("2temp #s : " +originOffset + " " + tempZoom);
                    VUE.getActiveViewer().loadFocal(slide);

                    // 2008-06-17 SMF: there is currently no "active slide" that is
                    // attended to, so this has never done anything.  Up/down arrows
                    // appear to work fine right now, so just leaving this out for now.
                    // If we find a problem, what we'd need to play with is setting
                    // the active LWComponent.class, not LWSlide.class

                    // update inspectors (optional -- may not actually want to do this, but
                    // currently required if you want up/down arrows to subsequently navigate
                    // the pathway)
                    //VUE.setActive(LWSlide.class, this, slide);
                }

            }

            // update inspectors (optional -- may not actually want to do this, but
            // currently required if you want up/down arrows to subsequently navigate
            // the pathway)

            //           long now = System.currentTimeMillis();
            //           MapMouseEvent mme = new MapMouseEvent(new MouseEvent(VUE.getActiveViewer(),
            //                                                                      MouseEvent.MOUSE_CLICKED,
            //                                                                      now,
            //                                                                      5,5,5,5,
            //                                                                      false));
            //           ((LWSlide)c).getPathwayEntry().pathway.getMasterSlide().doZoomingDoubleClick(mme);
        }
    };

    private static boolean hasSyncable(LWSelection s) {
        return getSyncable(s) != null;
    }

    //     private static LWSlide getSyncable(LWSelection s) {
    //         return getSyncable(true);
    //     }

    private static LWSlide getSyncable(LWSelection s) {
        if (s.only() instanceof LWSlide && ((LWSlide) s.only()).canSync()) {
            return (LWSlide) s.only();
        } else {
            LWPathway.Entry e = VUE.getActiveEntry();
            if (e != null && e.canProvideSlide() && !e.isMapView())
                return e.getSlide();
        }
        return null;
    }

    public static final LWCAction SyncToNode = new LWCAction(
            VueResources.local("mapViewer.componentMenu.syncMenu.slide2node")) {
        boolean enabledFor(LWSelection s) {
            return hasSyncable(s);
        }

        public void act(LWSelection s) {
            Slides.synchronizeSlideToNode(getSyncable(s));
        }

        public String getUndoName() {
            return "Sync";
        }
    };

    public static final LWCAction SyncToSlide = new LWCAction(
            VueResources.local("mapViewer.componentMenu.syncMenu.node2slide")) {
        boolean enabledFor(LWSelection s) {
            return hasSyncable(s);
        }

        public void act(LWSelection s) {
            Slides.synchronizeNodeToSlide(getSyncable(s));
        }

        public String getUndoName() {
            return "Sync";
        }
    };

    public static final LWCAction SyncAll = new LWCAction(
            VueResources.local("mapViewer.componentMenu.syncMenu.all")) {
        boolean enabledFor(LWSelection s) {
            return hasSyncable(s);
        }

        public void act(LWSelection s) {
            Slides.synchronizeAll(getSyncable(s));
        }

        public String getUndoName() {
            return "Sync";
        }
    };

    public static final LWCAction RemoveResourceAction = new LWCAction(
            VueResources.local("mapViewer.componentMenu.removeResource.label")) {
        public void act(LWComponent c) {
            final LWSelection sel = new LWSelection();
            Resource resource = c.getResource();
            //sel.clear();
            URLResource nullResource = null;
            c.setResource(nullResource);

            if (c.hasChildren()) {
                List<LWComponent> children = c.getChildren();
                Iterator<LWComponent> childIterator = children.iterator();

                while (childIterator.hasNext()) {
                    LWComponent comp = childIterator.next();
                    if (comp instanceof LWImage) {
                        LWImage image = ((LWImage) comp);
                        if (image.getResource().equals(resource))
                            sel.add(comp);
                    }
                }

                for (LWContainer parent : sel.getParents()) {
                    if (DEBUG.Enabled)
                        info("deleting for parent " + parent);
                    parent.deleteChildrenPermanently(sel);

                    // someday: would be nice if this could simply be
                    // handled as a traversal on the map: pass down
                    // the list of items to remove, and any parent
                    // that notices one of it's children removes it.

                }
            }
        }
    };

    public static final LWCAction RemoveResourceKeepImageAction = new LWCAction(
            VueResources.local("mapViewer.componentMenu.removeResourceKeepImage.label")) {
        public void act(LWComponent c) {

            URLResource nullResource = null;
            c.setResource(nullResource);
        }
    };

    //m.add(Actions.AddURLAction);
    //    m.add(Actions.RemoveResourceAction);
    public static final Action NotesAction = new NotesActionClass(
            MENU_INDENT + VueResources.local("mapViewer.componentMenu.notes.label"));
    public static final Action ContextNotesAction = new NotesActionClass(VueResources.local("actions.addnotes"));

    public static class NotesActionClass extends VueAction {
        public NotesActionClass(String s) {
            super(s);
        }

        public void act() {

            //GUI.makeVisibleOnScreen(VUE.getInfoDock());
            VUE.getInfoDock().setVisible(true);
            VUE.getInfoDock().setRolledUp(false, true);
            VUE.getInfoDock().raise();
            VUE.getInspectorPane().showNotesView();
        }
        //public void act() { VUE.ObjectInspector.setVisible(true); }
    };

    public static final Action InfoAction = new VueAction(
            VueResources.local("mapViewer.componentMenu.info.label")) {
        public void act() {
            VUE.getInspectorPane().showInfoView();
            GUI.makeVisibleOnScreen(this, tufts.vue.ui.InspectorPane.class);
            VUE.getInfoDock().setRolledUp(false, true);
        }
        //public void act() { VUE.ObjectInspector.setVisible(true); }
    };

    //-------------------------------------------------------
    // Group/Ungroup
    //-------------------------------------------------------

    public static final Action Group = new LWCAction(VueResources.local("menu.format.group"),
            keyStroke(KeyEvent.VK_G, COMMAND), "/tufts/vue/images/xGroup.gif") {
        boolean mayModifySelection() {
            return true;
        }

        boolean enabledFor(LWSelection s) {

            // TODO: allow even if all DON'T have same parent: e.g., if you select
            // all, and this includes the children of some nodes selected, still allow
            // everything into one group, and just ignore the children of the non-map.
            // E.g., implement as a special case: if multiple parents, and at least
            // one has the map has a parent, grab all elements in selection that are also
            // children of the map, and group them.

            // Would be nice to fully know up front if we're going to allow the grouping tho.
            // E.g., if either all have same parent, or there's at least two items in the
            // group which have the map as a parent.  Could easily have the selection
            // keep a count for each parent class type encountered (in a hash).

            // As long as doing that, might as well keep a hash of all types in selection,
            // tho we only appear to ever use this for checking the group count (maybe special
            // case).

            //return s.size() >= 2;

            // enable only when two or more objects in selection,
            // and all share the same parent
            //return s.size() >= 2 && s.allHaveSameParent();

            // below condition doesn't allow explicit grouping of links -- was this causing trouble somewhere?

            return ((s.size() - s.count(LWLink.class)) >= 2 && s.allHaveSameParent()
                    && !(VUE.getActiveViewer().getFocal() instanceof LWSlide));
        }

        void act(LWSelection s) {
            if (s.size() == 2 && s.count(LWGroup.class) == 1) {
                // special case: join the group (really need another action for this)
                LWGroup toJoin;
                LWComponent toAdd;
                if (s.first() instanceof LWGroup) {
                    toJoin = (LWGroup) s.first();
                    toAdd = s.last();
                } else {
                    toJoin = (LWGroup) s.last();
                    toAdd = s.first();
                }
                toJoin.addChild(toAdd);
            } else {
                LWContainer parent = s.first().getParent(); // all have same parent
                LWGroup group = LWGroup.create(s);
                parent.addChild(group);
                VUE.getSelection().setTo(group);
            }
        }
    };

    /**
     * If there are any groups in the selection, those groups will be dispersed, and
     * everything else in selection is ignored.
     * Otherwise, if everything in the selection has the same parent group,
     * they'll all be removed from that group.
     * If neither of the above conditions are met, the action is disabled.
     *
     * If groups were dispersed, the selection will be set to the contents of the
     * dispersed groups.
     */
    public static final LWCAction Ungroup =
            //new LWCAction("Ungroup", keyStroke(KeyEvent.VK_G, COMMAND+SHIFT), "/tufts/vue/images/GroupGC.png") {
            //new LWCAction("Ungroup", keyStroke(KeyEvent.VK_G, COMMAND+SHIFT), "/tufts/vue/images/GroupUnGC.png") {
            new LWCAction(VueResources.local("menu.format.ungroup"), keyStroke(KeyEvent.VK_G, COMMAND + SHIFT),
                    "/tufts/vue/images/xUngroup.png") {
                boolean mayModifySelection() {
                    return true;
                }

                boolean enabledFor(LWSelection s) {
                    return s.count(LWGroup.class) > 0 || s.allHaveSameParentOfType(LWGroup.class);
                }

                void act(LWSelection s) {
                    final Collection<LWComponent> toSelect = new HashSet(); // ensure no duplicates
                    if (s.count(LWGroup.class) > 0) {
                        if (DEBUG.EVENTS)
                            out("Ungroup: dispersing any selected groups");
                        disperse(s, toSelect);
                    } else {
                        if (DEBUG.EVENTS)
                            out("Ungroup: de-grouping any selected inside a group");
                        degroup(s, toSelect);
                    }

                    if (toSelect.size() > 0)
                        VUE.getSelection().setTo(toSelect);
                    else
                        VUE.getSelection().clear();
                }

                private void degroup(Iterable<LWComponent> iterable, Collection toSelect) {
                    final List<LWComponent> removing = new ArrayList();

                    for (LWComponent c : iterable) {
                        if (c.getParent() instanceof LWGroup) {
                            //if (LWLink.LOCAL_LINKS && c instanceof LWLink && ((LWLink)c).isConnected()) {
                            if (c instanceof LWLink && ((LWLink) c).isConnected()) {
                                // links control their own parentage when connected
                                continue;
                            } else
                                removing.add(c);
                        }
                    }

                    // This action only enabled if all the selected components have
                    // exactly the same parent group.

                    if (removing.size() > 0) {
                        final LWComponent first = (LWComponent) removing.get(0);
                        final LWGroup group = (LWGroup) first.getParent(); // the group losing children
                        final LWContainer newParent = group.getParent();
                        group.removeChildren(removing); // more control & efficient events
                        newParent.addChildren(removing);
                        toSelect.addAll(removing);

                        // LWGroups now handle auto-dispersal themseleves if all children are removed,
                        // so we don't ened to worry about auto-dispersing any groups that end up
                        // up with less than two children in them.

                    }

                    //VUE.getSelection().setTo(toSelect);

                }

                private void disperse(Iterable<LWComponent> iterable, Collection toSelect) {
                    for (LWComponent c : iterable) {
                        if (c instanceof LWGroup) {
                            toSelect.addAll(c.getChildren());
                            ((LWGroup) c).disperse();
                        }
                    }
                }
            };

    public static final LWCAction Rename = new LWCAction(VueResources.local("menu.edit.rename"),
            VueUtil.isWindowsPlatform() ? keyStroke(KeyEvent.VK_F2) : keyStroke(KeyEvent.VK_ENTER)) {
        boolean undoable() {
            return false;
        } // label editor handles the undo

        boolean enabledFor(LWSelection s) {
            return s.size() == 1 && s.first().supportsUserLabel() && !s.first().isLocked();
        }

        void act(LWComponent c) {
            // todo: throw interal exception if c not in active map
            // todo: not working in slide viewer...

            // BUG: can happen on hitting enter in the search box when a single node selected SMF logged 2012-06-10 19:40.28 Sunday SFAir.local
            // Fixed by changing SearchTextField key handlers to operate on keyPressed v.s. keyReleased and being sure to consume the event.
            //if (VUE.mSearchTextField.hasFocus()) return;

            VUE.getActiveViewer().activateLabelEdit(c);
        }
    };

    /*
      // doesn't help unless is actually in the VueMenuBar -- change this, so all keystrokes
      // are processed if in menu bar or not (hack into VueMenuBar, or maybe FocusManger?)
    public static final Action Rename2 =
    new LWCAction("Rename", VueUtil.isWindowsPlatform() ? keyStroke(KeyEvent.VK_ENTER) : keyStroke(KeyEvent.VK_F2)) {
    boolean undoable() { return false; } // label editor handles the undo
    boolean enabledFor(LWSelection s) {
        return s.size() == 1 && s.first().supportsUserLabel();
    }
    void act(LWComponent c) {
        // todo: throw interal exception if c not in active map
        VUE.getActiveViewer().activateLabelEdit(c);
    }
    };
    */

    //-------------------------------------------------------
    // Arrange actions
    //-------------------------------------------------------

    public static final LWCAction BringToFront = new LWCAction(
            VueResources.local("menu.format.arrange.bringtofront"),
            VueResources.local("menu.format.arrange.bringtofront.tooltip"), keyStroke(KeyEvent.VK_F, ALT)) {
        boolean enabledFor(LWSelection s) {
            if (s.size() == 1)
                return true;
            //return !s.first().getParent().isOnTop(s.first()); // todo: not always getting updated
            return s.size() >= 2;
        }

        void act(LWSelection selection) {
            LWContainer.bringToFront(selection);
        }
    };
    public static final LWCAction SendToBack = new LWCAction(VueResources.local("menu.format.arrange.sendtoback"),
            VueResources.local("menu.format.arrange.sendtoback.tooltip"), keyStroke(KeyEvent.VK_B, ALT)) {
        boolean enabledFor(LWSelection s) {
            if (s.size() == 1)
                return true;
            //return !s.first().getParent().isOnBottom(s.first()); // todo: not always getting updated
            return s.size() >= 2;
        }

        void act(LWSelection selection) {
            LWContainer.sendToBack(selection);
        }
    };

    public static final LWCAction BringForward = new LWCAction(
            VueResources.local("menu.format.arrange.bringforward")) {
        boolean enabledFor(LWSelection s) {
            return BringToFront.enabledFor(s);
        }

        void act(LWSelection selection) {
            LWContainer.bringForward(selection);
        }
    };

    public static final LWCAction SendBackward = new LWCAction(
            VueResources.local("menu.format.arrange.sendbackward")) {
        boolean enabledFor(LWSelection s) {
            return SendToBack.enabledFor(s);
        }

        void act(LWSelection selection) {
            LWContainer.sendBackward(selection);
        }
    };

    //-------------------------------------------------------
    // Font/Text Actions
    //-------------------------------------------------------

    public static final LWCAction FontSmaller = new LWCAction(VueResources.local("menu.format.font.fontsmaller"),
            keyStroke(KeyEvent.VK_MINUS, COMMAND + SHIFT)) {
        void act(LWComponent c) {
            int size = c.mFontSize.get();
            if (size > 1) {
                if (size >= 14 && size % 2 == 0)
                    size -= 2;
                else
                    size--;
                c.mFontSize.set(size);
            }
        }
    };
    public static final LWCAction FontBigger = new LWCAction(VueResources.local("menu.format.font.fontbig"),
            keyStroke(KeyEvent.VK_EQUALS, COMMAND + SHIFT)) {
        void act(LWComponent c) {
            int size = c.mFontSize.get();
            if (size >= 12 && size % 2 == 0)
                size += 2;
            else
                size++;
            c.mFontSize.set(size);
        }
    };
    public static final LWCAction FontBold = new LWCAction(VueResources.local("menu.format.font.fontbold"),
            keyStroke(KeyEvent.VK_B, COMMAND)) {
        void act(LWComponent c) {
            c.mFontStyle.set(c.mFontStyle.get() ^ Font.BOLD);
        }
    };
    public static final LWCAction FontItalic = new LWCAction(VueResources.local("menu.format.font.fontitalic"),
            keyStroke(KeyEvent.VK_I, COMMAND)) {
        void act(LWComponent c) {
            c.mFontStyle.set(c.mFontStyle.get() ^ Font.ITALIC);
        }
    };

    public static final LWCAction FontUnderline = new LWCAction(
            VueResources.local("menu.format.font.fontunderline"), keyStroke(KeyEvent.VK_U, COMMAND)) {
        void act(LWComponent c) {
            c.mFontUnderline.set((c.mFontUnderline.get().toString()).equals("underline") ? "normal" : "underline");

        }
    };

    /** this will toggle the collapsed state flag on the selected nodes */
    public static final LWCAction Collapse = new LWCAction(VueResources.local("menu.view.collapse")) {
        boolean enabledFor(LWSelection s) {
            final int nodeCount = s.count(LWNode.class);
            return nodeCount > 1 || s.size() == 1 && s.only().hasChildren();
        }

        @Override
        void act(LWComponent c) {
            c.setCollapsed(!c.isCollapsed());
        }
    };

    public static final VueAction ToggleGlobalCollapse = new VueAction(VueResources.local("menu.view.collapseAll"),
            keyStroke(KeyEvent.VK_K, COMMAND)) {
        public void act() {

            LWComponent.toggleGlobalCollapsed();
            VUE.layoutAllMaps(LWComponent.Flag.COLLAPSED);
            viewer().getFocal().notify(this, LWKey.Repaint);

            // Currently, this action is ONLY fired via a menu item.  If other code
            // points might set this directly, this should be changed to a toggleState
            // action (impl getToggleState), and those code points should call this
            // action to do the toggle, so the menu item checkbox state will stay
            // synced.
        }

        @Override
        public boolean overrideIgnoreAllActions() {
            return true;
        }

    };

    // TODO: need a ViewerAction subclass of VueAction that is for
    // actions that are only enabled as long as there is an active viewer
    // (also may want a MapAction subclass?  should be same semantics -- we don't support empty MapViewer's)

    public static final VueAction ViewBackward = new VueAction(VueResources.local("menu.view.backward"),
            VueResources.local("menu.view.backward.tooltip"), keyStroke(KeyEvent.VK_LEFT, COMMAND), null) {
        public void act() {
            viewer().viewBackward();
        }
    };
    public static final VueAction ViewForward = new VueAction(VueResources.local("menu.view.forward"),
            VueResources.local("menu.view.forward.tooltip"), keyStroke(KeyEvent.VK_RIGHT, COMMAND), null) {
        public void act() {
            viewer().viewForward();
        }
    };

    static {
        new MapViewer.Listener() {
            {
                EventHandler.addListener(MapViewer.Event.class, this);
            } // this ref only thing preventing GC 

            public void eventRaised(MapViewer.Event e) {

                if (e.id != MapViewer.Event.VIEWS_CHANGED)
                    return;

                if (e.viewer != null) {
                    ViewBackward.setEnabled(e.viewer.hasBackwardViews());
                    ViewForward.setEnabled(e.viewer.hasForwardViews());
                } else {
                    // todo: never happens -- provide auxillary somewhere event just for this case?
                    ViewBackward.setEnabled(false);
                    ViewForward.setEnabled(false);
                }
            }

            public String toString() {
                return getClass().getEnclosingClass().getName() + "(back/forward menu updater)";
            }
        };
    }

    private static class Stats {
        float minX, minY;
        float maxX, maxY;
        float centerX, centerY;
        float totalWidth, totalHeight; // added width/height of all in selection
        float maxWide, maxTall; // width of widest, height of tallest
    }

    //-------------------------------------------------------
    // Arrange actions
    //
    // todo bug: if items have a stroke width, there is an
    // error in adjustment such that repeated adjustments
    // nudge all the nodes by what looks like half the stroke width!
    // (error occurs even in first adjustment, but easier to notice
    // in follow-ons)
    //-------------------------------------------------------

    public abstract static class ArrangeAction extends LWCAction {
        static float minX, minY;
        static float maxX, maxY;
        static float centerX, centerY;
        static float oldCenterX = Float.NaN, oldCenterY = Float.NaN;
        static float totalWidth, totalHeight; // added width/height of all in selection
        static float maxWide, maxTall; // width of widest, height of tallest
        static double radiusWide, radiusTall;
        // note: static variables; obviously not thread-safe here

        private ArrangeAction(String name, KeyStroke keyStroke) {
            super(name, keyStroke);
        }

        private ArrangeAction(String name, int keyCode) {
            super(name, keyStroke(keyCode, COMMAND + SHIFT));
        }

        private ArrangeAction(String name) {
            super(name);
        }

        boolean mayModifySelection() {
            return true;
        }

        boolean enabledFor(LWSelection s) {
            return s.size() >= 2 || (s.size() == 1 && s.first().getParent() instanceof LWSlide); // todo: a have capability check (free-layout?  !isLaidOut() ?)
        }

        boolean supportsSingleMover() {
            return true;
        }

        public void act(List<? extends LWComponent> bag) {
            act(new LWSelection(bag));
        }

        void act(LWSelection selection) {
            LWComponent singleMover = null;

            Rectangle2D.Float r = null; // will be the total bounds area we're going to layout into

            if (supportsSingleMover() && selection.size() == 1
                    && selection.first().getParent() instanceof LWSlide) { // todo: capability check
                singleMover = selection.first();
                r = singleMover.getParent().getZeroBounds();
            } else if (!selection.allOfType(LWLink.class)) {
                Iterator<LWComponent> i = selection.iterator();
                while (i.hasNext()) {
                    LWComponent c = i.next();
                    // remove all links from our cloned copy of the selection
                    if (c instanceof LWLink)
                        i.remove();
                    // remove all children of nodes or groups, who's parent handles their layout
                    // need to allow for in-group components now.
                    // todo: unexpected behaviour if some in-group and some not?
                    if (c.isManagedLocation())
                        i.remove();
                }
            }

            // TODO: do we need to recompute statistics in the selection?  E.g., links from another
            // layer in selection should be removed.
            // TODO: change mParents in LWSelection to be a multi-set, then just do the arrange
            // based on the most top level parent with the most entries (even just doing the
            // first most top-level parent would handle most cases)
            if (selection.allHaveSameParent() || selection.allHaveTopLevelParent())
                ; // we're good
            else
                throw new DeniedException("all must have same or top-level parent");

            if (r == null)
                r = LWMap.getLayoutBounds(selection);

            if (selection.isSized()) {
                r.width = selection.getWidth();
                r.height = selection.getHeight();
            }

            computeStatistics(r, selection);

            if (singleMover != null) {
                // If we're a single selected object laying out in a parent,
                // only bother to arrange that one object -- make sure
                // we can never touch the parent (it used to be added to
                // the selection above to compute our total bounds, tho we do
                // that manually now).
                arrange(singleMover);
            } else {
                arrange(selection);
            }
        }

        static protected void computeStatistics(Rectangle2D.Float r, Collection<LWComponent> nodes) {

            if (r == null)
                r = LWMap.getLayoutBounds(nodes);

            minX = r.x;
            minY = r.y;
            maxX = r.x + r.width;
            maxY = r.y + r.height;
            centerX = (minX + maxX) / 2;
            centerY = (minY + maxY) / 2;
            totalWidth = totalHeight = 0;
            maxWide = maxTall = 0;

            for (LWComponent c : nodes) {
                totalWidth += c.getWidth();
                totalHeight += c.getHeight();
                if (c.getWidth() > maxWide)
                    maxWide = c.getWidth();
                if (c.getHeight() > maxTall)
                    maxTall = c.getHeight();
            }
        }

        void arrange(LWSelection selection) {
            for (LWComponent c : selection) {
                arrange(c);
            }
        }

        /**
        void arrange(LWSelection selection,float centerX,float centerY) {
           for (LWComponent c : selection)
            arrange(c,centerX,centerY);
        }
        */

        void arrange(LWComponent c) {
            throw new RuntimeException("unimplemented arrange action");
        }

        /**
        void arrange(LWComponent c,float centerX,float centerY) {
           arrange(c);
        }
        */
        protected static void clusterNodesAbout(final LWComponent center,
                final Collection<LWComponent> clustering) {

            if (DEBUG.Enabled)
                Log.debug("clusterNodesAbout: " + center + ": " + Util.tags(clustering));

            // recording the current action time on the centering node can later help
            // us determine the layout priority for new data items when adding to the map
            // (by looking at the most recent clustering centers)
            center.setClientData(tufts.vue.ds.DataAction.ClusterTimeKey, currentActionTime);

            final LWContainer commonParent = center.getParent();
            final List<LWComponent> toReparent = new ArrayList();

            // this is important both to remove any linked that may be our descendents, as
            // well as grab any linked that are currently children of something else
            // (unfortunately, this will also grab them out of other layers if they were there,
            // which isn't technically needed, but okay for now).
            for (LWComponent c : clustering) {
                if (c.getParent() != commonParent)
                    toReparent.add(c);
            }

            //-----------------------------------------------------------------------------
            // TODO: if center is a child of any one of cluster, remove it first!
            // That way we can go back and forth between different relationship priorities & styles.
            // (possibly if it's a child of anything?)
            //-----------------------------------------------------------------------------

            if (toReparent.size() > 0) {
                if (toReparent.contains(commonParent)) {
                    // TOFIX: if we attempt to cluster a child node linked to it's parent, we get this:
                    throw new Error("clusterNodesAbout: setup failure, toReparent contains commonParent");
                }
                commonParent.addChildren(toReparent, LWComponent.ADD_CHILD_TO_SIBLING);
            }

            computeStatistics(null, clustering);
            centerX = center.getMapCenterX(); // should probably be local center, not map center
            centerY = center.getMapCenterY();

            // todo: bump up if there are link labels to make room for
            // also, vertical diameter should be enough to stack half the nodes (half of totalHeight) vertically
            // add an analyize to ArrangeAction which we can use here to re-compute on the new set of linked nodes
            //radiusWide = center.getWidth() / 2 + maxWide / 2 + 50;
            //             radiusWide = Math.max(totalWidth/8,  center.getWidth() / 2 + maxWide / 2 + 50);
            //             radiusTall = Math.max(totalHeight/8, center.getHeight() / 2 + maxTall / 2 + 50);
            radiusWide = center.getWidth() / 2 + maxWide / 2 + 50;
            radiusTall = center.getHeight() / 2 + maxTall / 2 + 50;

            //clusterNodes(centerX, centerY, radiusWide, radiusTall, linked);
            clusterNodes(clustering);

        }

        public static void clusterLinked(final LWComponent center) {
            if (DEBUG.Enabled)
                Log.debug("clustering linked " + center);

            clusterNodesAbout(center, center.getClustered());
        }

        // todo: smarter algorithm that lays out concentric rings, with more nodes in
        // each larger ring (compute ellipse circumference); tricky: either need a good
        // guess at the number of rings, or just leave the last ring far more spread out
        // (remainder nodes will be left for the last right

        public static void clusterNodes(Collection<LWComponent> nodes) {
            // todo: if a link-chain detected, lay out in link-order e.g., start with
            // any non-linked nodes, then find any with one link (into our set), and
            // then follow the link chain laying out any nodes found in our selection
            // first (removing them from the layout list), then continue to the next
            // node, etc.  Also, can prefer link directionality if there are arrow
            // heads.

            final double slice = (Math.PI * 2) / nodes.size();

            final int maxTierSize = 20;
            final int tiers = nodes.size() / maxTierSize;
            //final int tiers = 3;

            final double startAngle;

            if (nodes.size() == 1) {
                // Add 90 degrees so the "clock" starts at bottom (the single clustered item appears at the bottom)
                startAngle = Math.PI / 2;
            } else {
                // Add 270 degrees so the "clock" starts at the top -- so something
                // will be laid out at exactly the 12 o'clock position
                startAngle = Math.PI / 2 * 3;
            }

            Color fill = Color.white;

            int i = 0;

            // TODO: if we keep the spiral layout, could note what Field we're clustering
            // on (if any), and find the field with the next highest number of enumerated
            // values, and auto-organize by that value (which you wouldn't see until
            // you did the search, but would be a niceity)

            if (nodes.size() > maxTierSize) {

                //------------------------------------------------------------------
                // tiered circular layout or "spiral" -- begins to spiral beyond 2 tiers,
                // and has a distinct spiral appearance upwards of about 100 nodes
                // (when nodes are small and uniform)
                //------------------------------------------------------------------

                for (LWComponent c : nodes) {
                    final double angle = startAngle + slice * i;
                    final int tier = i % tiers;
                    final double factor = 1 + tier * 0.33;
                    final double rwide = radiusWide * factor;
                    final double rtall = radiusTall * factor;
                    c.setCenterAt(centerX + rwide * Math.cos(angle), centerY + rtall * Math.sin(angle));
                    i++;

                    //c.setFillColor(fill);
                    //fill = Util.factorColor(fill, 0.99);
                }

            } else {

                //------------------------------------------------------------------
                // pure circular layout
                //------------------------------------------------------------------

                for (LWComponent c : nodes) {
                    final double angle = startAngle + slice * i++;
                    c.setCenterAt(centerX + radiusWide * Math.cos(angle), centerY + radiusTall * Math.sin(angle));
                }
            }

        }

        //         protected void old_clusterNodes(Collection<LWComponent> nodes)
        //         {
        //             // todo: if a link-chain detected, lay out in link-order e.g., start with
        //             // any non-linked nodes, then find any with one link (into our set), and
        //             // then follow the link chain laying out any nodes found in our selection
        //             // first (removing them from the layout list), then continue to the next
        //             // node, etc.  Also, can prefer link directionality if there are arrow
        //             // heads.

        //             final double slice = (Math.PI * 2) / nodes.size();
        //             int i = 0;

        //             final int maxTierSize = 20;
        //             final int tiers = nodes.size() / maxTierSize;
        //             //final int tiers = 3;

        //             java.awt.Color fill = java.awt.Color.white;

        //             for (LWComponent c : nodes) {
        //                 // We add Math.PI/2*3 (270 degrees) so the "clock" always starts at the top -- so something
        //                 // is always is laid out at exactly the 12 o'clock position
        //                 final double angle = Math.PI/2*3 + slice * i;

        //                 if (false && nodes.size() > 200) {
        //                     // random layout
        //                     double rand = Math.random()+.1;
        //                     c.setCenterAt(centerX + radiusWide * rand * Math.cos(angle),
        //                                   centerY + radiusTall * rand * Math.sin(angle));

        //                 } else if (nodes.size() > maxTierSize) {
        //                     // tiered circular layout -- begins to spiral beyond 2 tiers
        //                     final int tier = i % tiers;
        //                     final double factor = 1 + tier * 0.33;
        //                     final double rwide = radiusWide * factor;
        //                     final double rtall = radiusTall * factor;
        // //                     final double rwide = (radiusWide / tiers) * (tier+1);
        // //                     final double rtall = (radiusTall / tiers) * (tier+1);
        //                     c.setCenterAt(centerX + rwide * Math.cos(angle),
        //                                   centerY + rtall * Math.sin(angle));
        //                          if (tier == 0) c.setFillColor(Color.magenta);
        //                     else if (tier == 1) c.setFillColor(Color.red);
        //                     else if (tier == 2) c.setFillColor(Color.green);
        //                     else if (tier == 3) c.setFillColor(Color.blue);
        //                 } else {

        //                     // circular layout
        //                     c.setCenterAt(centerX + radiusWide * Math.cos(angle),
        //                                   centerY + radiusTall * Math.sin(angle));
        //                 }

        //                 i++;

        //                 //c.setFillColor(fill);
        //                 //fill = Util.factorColor(fill, 0.99);

        //             }

        //         }

    };

    public static LWComponent[] sortByX(LWComponent[] array) {
        java.util.Arrays.sort(array, LWComponent.XSorter);
        return array;
    }

    public static LWComponent[] sortByY(LWComponent[] array) {
        java.util.Arrays.sort(array, LWComponent.YSorter);
        return array;
    }

    public static final Action FillWidth = new ArrangeAction(VueResources.local("actions.fillwidth")) {
        void arrange(LWComponent c) {
            c.setFrame(minX, c.getY(), maxX - minX, c.getHeight());
        }
    };
    public static final Action FillHeight = new ArrangeAction(VueResources.local("actions.fillheight")) {
        void arrange(LWComponent c) {
            c.setFrame(c.getX(), minY, c.getWidth(), maxY - minY);
        }
    };

    public static class NudgeAction extends LWCAction {
        final int osdx, osdy; // on-screen delta-x, delta-y

        NudgeAction(int dx, int dy, String name, KeyStroke stroke) {
            super(name, stroke);
            osdx = dx;
            osdy = dy;
        }

        public static boolean enabledOn(LWSelection s) {
            return s.size() > 0 && s.first().isMoveable() && VUE.getActiveViewer().isFocusOwner()
                    && !(VUE.getActiveSubTool() instanceof tufts.vue.SelectionTool.Browse);
        }

        @Override
        boolean enabledFor(LWSelection s) {
            return enabledOn(s);
        }

        @Override
        void act(LWComponent c) {
            nudgeOrReorder(c, osdx, osdy);
        }

        private void nudgeOrReorder(LWComponent c, int x, int y) {

            //             if (VUE.getActiveSubTool() instanceof tufts.vue.SelectionTool.Browse) {
            //                 Log.debug("nudge disabled during browse");
            //                 return;
            //             }

            if (c.getParent() instanceof LWNode) { // TODO: a more abstract test... inVisuallyOrderedContainer?
                if (x < 0 || y < 0)
                    c.getParent().sendBackward(c);
                else
                    c.getParent().bringForward(c);
            } else {
                // With relative coords, if we want to enforce a certian on-screen pixel change,
                // we need to adjust for the current zoom, as well as the net map scaling present
                // in the parent of the moving object.
                final double unit = VUE.getActiveViewer().getZoomFactor() * c.getParent().getMapScale();
                final float dx = (float) (x / unit);
                final float dy = (float) (y / unit);
                c.translate(dx, dy);
            }
        }
    }

    private static final int PUSH_DISTANCE = 24;

    private static boolean enabledForPushPull(LWSelection s) {
        return s.size() == 1 && !(s.first() instanceof LWLink) // links giving us trouble
                && s.first().getParent() instanceof LWMap.Layer; // don't allow pushing inside slides, nodes / anything
    }

    public static final LWCAction PushOutLinked = new LWCAction(VueResources.local("menu.format.arrange.pushout"),
            keyStroke(KeyEvent.VK_CLOSE_BRACKET, ALT)) {

        boolean enabledFor(LWSelection s) {
            return enabledForPushPull(s);
        }

        // todo: for selection size > 1, push on bounding box
        void act(LWComponent c) {
            // although we don't currently want to support pushing inside anything other than
            // a layer, this generic call would handle other cases if we can support them
            projectNodes(c, PUSH_DISTANCE, PUSH_LINKED);
            // currenly only pushes within a single layer: provide the map
            // as the focal if want to push in all layers
            //pushNodes(viewer().getDropFocal(), c); // push in active focal: will work for slides also
            // active focal should normally be a layer otherwise
            // ideally would ask the node for it's layer, as theoretically we could be dropping into
            // one layer then push in another
            //pushNearbyNodes(viewer().getMap(), c);
            //pushNearbyNodes(viewer().getDropFocal(), c);

        }
    };

    public static final LWCAction PushOut = new LWCAction(VueResources.local("menu.format.arrange.pushout"),
            keyStroke(KeyEvent.VK_EQUALS, ALT)) {

        boolean enabledFor(LWSelection s) {
            //   return enabledForPushPull(s);
            return s.size() >= 1;
        }

        @Override
        void act(LWSelection s) {
            if (s.size() == 1) {
                act(s.get(0));
            } else {
                LayoutAction.stretch.act(s);
            }
        }

        // todo: for selection size > 1, push on bounding box
        @Override
        public void act(LWComponent c) {
            // although we don't currently want to support pushing inside anything other than
            // a layer, this generic call would handle other cases if we can support them
            projectNodes(c, PUSH_DISTANCE, PUSH_ALL);
            // currenly only pushes within a single layer: provide the map
            // as the focal if want to push in all layers
            //pushNodes(viewer().getDropFocal(), c); // push in active focal: will work for slides also
            // active focal should normally be a layer otherwise
            // ideally would ask the node for it's layer, as theoretically we could be dropping into
            // one layer then push in another
            //pushNearbyNodes(viewer().getMap(), c);
            //pushNearbyNodes(viewer().getDropFocal(), c);

        }
    };

    public static final LWCAction PullInLinked = new LWCAction(VueResources.local("menu.format.arrange.pullin"),
            keyStroke(KeyEvent.VK_OPEN_BRACKET, ALT)) {
        boolean enabledFor(LWSelection s) {
            return enabledForPushPull(s);
        }

        void act(LWComponent c) {
            projectNodes(c, -PUSH_DISTANCE, PUSH_LINKED);

        }
    };

    public static final LWCAction PullIn = new LWCAction(VueResources.local("menu.format.arrange.pullin"),
            keyStroke(KeyEvent.VK_MINUS, ALT)) {
        boolean enabledFor(LWSelection s) {
            return enabledForPushPull(s);
        }

        void act(LWComponent c) {
            projectNodes(c, -PUSH_DISTANCE, PUSH_ALL);

        }
    };

    private static final boolean DEBUG_PUSH = false;
    public static final Object PUSH_ALL = "pushAll";
    public static final Object PUSH_LINKED = "pushLinked";

    /** pushing must be a member of a map -- cannot push non-map member nodes.  todo: allow passing in of the map for this */
    public static void projectNodes(final LWComponent pushing, final int distance, Object pushKey) {
        Collection<LWComponent> toPush = null;

        if (pushKey == PUSH_LINKED && pushing.hasLinks())
            toPush = pushing.getLinked();

        if (toPush == null || toPush.size() == 0) {
            // ideally, this would push all the top level children in the current FOCAL
            toPush = pushing.getMap().getTopLevelItems(ChildKind.EDITABLE);
        }
        //toPush = pushing.getMap().getAllDescendents(); // only want top level -- especially, don't push children inside groups!
        //toPush = pushing.getParent().getChildren();

        projectNodes(toPush, pushing, distance);
    }

    // todo: combine into a Geometry.java with computeIntersection, computeConnector, projectPoint code from VueUtil
    // todo: to handle pushing inside slides, we'd need to get rid of the references to map bounds,
    // and always use local bounds
    public static void projectNodes(final Iterable<LWComponent> toPush, final LWComponent pusher,
            final int distance) {
        //             if (DEBUG.Enabled) Log.debug("projectNodes: "
        //                                          + "\n\t  pusher: " + pushing
        //                                          + "\n\t  toPush: " + Util.tags(toPush)
        //                                          + "\n\tdistance: " + distance
        //                                          );//,new Throwable("HERE"));

        if (DEBUG.Enabled)
            Log.debug("projectNodes: pusher=" + pusher);

        //pusher.getMapCenterY())
        //final Rectangle2D pushingRect = pushing.getMapBounds();

        final RectangularShape pushShape = pusher.getMapShape();

        final Collection exclude = java.util.Collections.singletonList(pusher);

        projectNodes(toPush, exclude, pusher, pushShape, distance);

    }

    private static void projectNodes(final Iterable<LWComponent> toPush, final Collection toExclude,
            final LWComponent pushing, // we want to remove this argument and only rely on pushShape, but we need alot more refactoring for that
            final RectangularShape pushShape, final int distance) {
        if (DEBUG.Enabled)
            Log.debug("projectNodes: " + "\n\t  pushing: " + pushing + "\n\tpushShape: " + pushShape
                    + "\n\t   toPush: " + Util.tags(toPush) + "\n\t distance: " + distance);//,new Throwable("HERE"));

        final Point2D.Float groundZero = new Point2D.Float((float) pushShape.getCenterX(),
                (float) pushShape.getCenterY());

        final java.util.List<LWComponent> links = new java.util.ArrayList();
        final java.util.List<LWComponent> nodes = new java.util.ArrayList();

        for (LWComponent node : toPush) {

            if (toExclude.contains(node))
                continue;

            if (node.isManagedLocation())
                continue;

            if (node instanceof LWLink) {
                LWLink link = (LWLink) node;
                if (link.isConnected() || link.isCurved()) // both cases are buggy right now
                    continue;
            }

            final Line2D.Float connector = new Line2D.Float();
            final boolean overlap = VueUtil.computeConnectorAndCenterHit(pushing, node, connector);
            //VueUtil.computeConnector(pushing, node, connector);

            Point2D newCenter = null;

            float adjust = distance;

            //final boolean intersects = node.intersects(pushingRect); // problems w/slide icons
            final boolean intersects = pushShape.intersects(node.getMapBounds());

            final boolean moveToEdge = overlap || intersects;

            if (false && DEBUG_PUSH) {
                // create a detached link from center of pushing to edge of each pushed to show vectors
                LWLink link = new LWLink();
                link.setHeadPoint(connector.getP1());
                link.setTailPoint(connector.getP2());
                link.setArrowState(LWLink.ARROW_TAIL);
                link.setNotes("head: " + pushing + "\ntail: " + node);
                links.add(link);
            }

            if (moveToEdge) {

                if (distance < 0) // do nothing further if pulling on
                    continue;

                // If overlapping, we want to move the node along a line away from the center
                // of the pushing node until it no longer overlaps.  As part of this process,
                // we compute the point at the edge of the pushing node that the overlapping
                // node would be at if all we were going to do was move it to the edge.  This
                // isn't strictly needed to produce the end result (we could start iterating
                // immediately, we don't need to start at the intersect), but it's useful for
                // debugging, and it may be a useful location to know for future tweaks to this
                // code.

                // first, find a point along the line from center of pushing to the center of node
                // that we know is outside of the pushing node
                final Point2D farOut = VueUtil.projectPoint(groundZero, connector, Short.MAX_VALUE);
                // now produce a ray that shoots from that point back to the center of the pushing node
                final Line2D.Float testRay = new Line2D.Float(farOut, groundZero);
                // now find the point at the edge of the pushing node that the ray intersects it
                final Point2D.Float intersect = VueUtil.computeIntersection(testRay, pushing);

                // now project the node along the connector line from the intersect
                // by small increments until the node no longer overlaps the
                // pushing node

                if (Util.isBadPoint(farOut) || Util.isBadPoint(intersect)) {
                    Log.warn("bad projection points:" + "\n\tgroundZero: " + Util.fmt(groundZero)
                            + "\n\t connector: " + Util.fmt(connector) + "\n\t    farOut: " + Util.fmt(farOut)
                            + "\n\t   testRay: " + Util.fmt(testRay) + "\n\t intersect: " + Util.fmt(intersect)
                            + "\n\t    pusher: " + pushing + "\n\t    pushee: " + node);
                } else {

                    for (int i = 0; i < 1000; i++) {
                        node.setCenterAt(VueUtil.projectPoint(intersect, connector, i * 2f));
                        //                         if (!node.intersects(pushingRect)) // problems w/slide icons
                        //                             break;
                        if (!pushShape.intersects(node.getMapBounds()))
                            break;
                        if (DEBUG_PUSH)
                            Log.debug("PUSH ITER " + i + " on " + node);
                    }
                }

                adjust /= 2; // we'll only push half the standard amount from here
            }

            newCenter = VueUtil.projectPoint(node.getMapCenterX(), node.getMapCenterY(), connector, adjust);

            if (Util.isBadPoint(newCenter)) {
                Log.error("bad newCenter: " + newCenter);
                newCenter = null;
            }

            if (DEBUG_PUSH) {
                float dist = (float) connector.getP1().distance(connector.getP2());
                String notes = String.format("distance: %.1f\nadjust: %.1f\n-center: %s\n+center: %s\nconnect: %s",
                        dist, adjust, Util.fmt(node.getMapCenter()), Util.fmt(newCenter), Util.fmt(connector));

                if (intersects)
                    notes += "\nINTERSECTS";
                if (overlap)
                    notes += "\nOVERLAP";

                final LWComponent n;

                if (false) {
                    n = node.duplicate();
                    node.setNotes(notes);
                    nodes.add(n);
                    n.setStrokeWidth(1);
                } else
                    n = node;

                if (moveToEdge) {
                    n.setTextColor(java.awt.Color.red);
                    n.mFontStyle.set(java.awt.Font.BOLD);
                }
                n.setNotes(notes);
                if (newCenter != null)
                    n.setCenterAt(newCenter);
            } else {
                if (newCenter != null)
                    node.setCenterAt(newCenter);
            }

        }

        if (DEBUG_PUSH) {
            pushing.getMap().sendToBack(pushing);
            pushing.getMap().addChildren(nodes);
            pushing.getMap().addChildren(links);
        }
    }

    // Note: if JScrollPane has focus, it will grap unmodified arrow keys.  If, say, a random DockWindow
    // has focus (e.g., not a field that would also grab arrow keys), they get through.
    // So the MapViewer has to specially check for these arrows keys to invoke these actions to
    // override it's parent JScrollPane.
    public static final LWCAction NudgeUp = new NudgeAction(0, -1, VueResources.local("menu.format.align.nudgeup"),
            keyStroke(KeyEvent.VK_UP));
    public static final LWCAction NudgeDown = new NudgeAction(0, 1,
            VueResources.local("menu.format.align.nudgedown"), keyStroke(KeyEvent.VK_DOWN));
    public static final LWCAction NudgeLeft = new NudgeAction(-1, 0,
            VueResources.local("menu.format.align.nudgeleft"), keyStroke(KeyEvent.VK_LEFT));
    public static final LWCAction NudgeRight = new NudgeAction(1, 0,
            VueResources.local("menu.format.align.nudgeright"), keyStroke(KeyEvent.VK_RIGHT));

    public static final LWCAction BigNudgeUp = new NudgeAction(0, -10,
            VueResources.local("menu.format.align.bignudgeup"), keyStroke(KeyEvent.VK_UP, SHIFT));
    public static final LWCAction BigNudgeDown = new NudgeAction(0, 10,
            VueResources.local("menu.format.align.bignudgedown"), keyStroke(KeyEvent.VK_DOWN, SHIFT));
    public static final LWCAction BigNudgeLeft = new NudgeAction(-10, 0,
            VueResources.local("menu.format.align.bignudgeleft"), keyStroke(KeyEvent.VK_LEFT, SHIFT));
    public static final LWCAction BigNudgeRight = new NudgeAction(10, 0,
            VueResources.local("menu.format.align.bignudgeright"), keyStroke(KeyEvent.VK_RIGHT, SHIFT));

    public static final ArrangeAction AlignTopEdges = new ArrangeAction(
            VueResources.local("menu.format.align.topedges"), keyStroke(KeyEvent.VK_UP, ALT)) {
        void arrange(LWComponent c) {
            c.setLocation(c.getX(), minY);
        }
    };
    public static final ArrangeAction AlignBottomEdges = new ArrangeAction(
            VueResources.local("menu.format.align.bottomedges"), keyStroke(KeyEvent.VK_DOWN, ALT)) {
        void arrange(LWComponent c) {
            c.setLocation(c.getX(), maxY - c.getHeight());
        }
    };
    public static final ArrangeAction AlignLeftEdges = new ArrangeAction(
            VueResources.local("menu.format.align.leftedges"), keyStroke(KeyEvent.VK_LEFT, ALT)) {
        void arrange(LWComponent c) {
            c.setLocation(minX, c.getY());
        }
    };
    public static final ArrangeAction AlignRightEdges = new ArrangeAction(
            VueResources.local("menu.format.align.rightedges"), keyStroke(KeyEvent.VK_RIGHT, ALT)) {
        void arrange(LWComponent c) {
            c.setLocation(maxX - c.getWidth(), c.getY());
        }
    };
    public static final ArrangeAction AlignCentersRow = new ArrangeAction(
            VueResources.local("menu.format.align.centerinrow"), keyStroke(KeyEvent.VK_R, ALT)) {
        void arrange(LWComponent c) {
            c.setLocation(c.getX(), centerY - c.getHeight() / 2);
        }
    };
    public static final ArrangeAction AlignCentersColumn = new ArrangeAction(
            VueResources.local("menu.format.align.centerincolumn"), keyStroke(KeyEvent.VK_C, ALT)) {
        void arrange(LWComponent c) {
            c.setLocation(centerX - c.getWidth() / 2, c.getY());
        }
    };

    //     public static final ArrangeAction OLDMakeCluster = new ArrangeAction(VueResources.local("menu.format.align.makecluster"), keyStroke(KeyEvent.VK_PERIOD, ALT)) {
    //             boolean supportsSingleMover() { return false; }
    //             boolean enabledFor(LWSelection s) { return s.size() > 0; }

    //             void arrange(LWSelection selection) {

    //                 final double radiusWide, radiusTall;

    //                 selection.resetStatistics(); // todo: why do we need to reset? is this a clone? (has no statistics)
    //                 if (DEBUG.Enabled) Log.debug("DATAVALUECOUNT: " + selection.getDataValueCount());
    //                 if (DEBUG.Enabled) Log.debug("DATA-ROW-COUNT: " + selection.getDataRowCount());

    //                 final int nDataValues = selection.getDataValueCount();
    //                 final int nDataRows = selection.getDataRowCount();

    //                 if (selection.size() == 1) {

    //                     // if a single item in selection, arrange all nodes linked to it in a circle around it

    //                     final LWComponent center = selection.first();
    //                     final Collection<LWComponent> linked = center.getLinked();

    // //                     final LWContainer commonParent = center.getParent();
    // //                     final List<LWComponent> toReparent = new ArrayList();
    // //                     // this is important both to remove any linked that may be our descendents, as
    // //                     // well as grab any linked that are currently children of something else
    // //                     // (unfortunately, this will also grab them out of other layers if they were there,
    // //                     // which isn't technically needed, but okay for now).
    // //                     for (LWComponent c : linked) {
    // //                         if (c.getParent() != commonParent)
    // //                             toReparent.add(c);
    // //                     }

    // //                     if (toReparent.size() > 0)
    // //                         commonParent.addChildren(toReparent, LWComponent.ADD_CHILD_TO_SIBLING);

    //                     clusterNodes(center, linked);

    //                     selection().setTo(center);
    //                     selection().add(linked);

    //                 }
    //                 else if (nDataValues == selection.size()) {

    //                     // If all the items in the selection are single enumerated data
    //                     // VALUES, (e.g., they were all selected by a single click on a
    //                     // field in the DataTree, selecting all values for that field) then
    //                     // perform a cluster operation on each value separately, clustering
    //                     // all connected rows/nodes around each value.

    //                     for (LWComponent center : selection)
    //                         clusterNodes(center, center.getLinked());

    //                 }
    //                 else if (nDataValues == 1 && nDataRows == (selection.size() - 1)) {

    //                     // If there's a single data VALUE in the selected, and everything
    //                     // ELSE is a data ROW, assume we really want to just do a clustering
    //                     // around the single data-value.  This is quite a leap to make
    //                     // given that the rows could be completely unrelated, but it's
    //                     // the most common use case at the moment.

    //                     // A more sane approach would be to extract the one value node,
    //                     // and do an arrange just with all other nodes found, and not
    //                     // care if they're data-nodes or linked nodes or not -- as long
    //                     // as we don't do anything nutty like arrange value nodes around
    //                     // each other, this should be fine.

    //                     Log.debug("guessing at an all-related data-values selection");

    //                     // find the one data value and cluster the rest around it

    //                     LWComponent center = null;

    //                     for (LWComponent c : selection) {
    //                         if (c.isDataValueNode()) {
    //                             center = c;
    //                             break;
    //                         }
    //                     }

    //                     clusterNodes(center, center.getLinked());

    //                 }
    //                 else {

    // //                     radiusWide = (maxX - minX) / 2;
    // //                     radiusTall = (maxY - minY) / 2;

    // //                     radiusWide = Math.max((maxX - minX) / 2, maxWide);
    // //                     radiusTall = Math.max((maxY - minY) / 2, maxTall);

    //                     radiusWide = Math.max((maxX - minX) / 2, totalWidth/4);
    //                     radiusTall = Math.max((maxY - minY) / 2, totalHeight/4);

    //                     //clusterNodes(centerX, centerY, radiusWide, radiusTall, selection);
    //                     clusterNodes(selection);

    //                     // The ring will expand on subsequent MakeCircle calls, because nodes are laid
    //                     // out on the ring on-center, but the bounds used to create the initial ring
    //                     // form the the top of the north-most mode to the bottom of the south-most node
    //                     // (same for east/west), which on the next call will be a bigger ring.  Would
    //                     // be hairy trying to figure out the the ring size that would contain the given
    //                     // nodes inside a given rectangle when laid-out on-center. [ Actually, would
    //                     // just computing the on-center bounds work? Better, but only perfectly if
    //                     // there's a node at exaclty N/S/E/W on the dial, and the ring-order (currently
    //                     // selection order, which is usually stacking order) hasn't changed.] If we
    //                     // want such functionality, would be better handled via a persistent "ring"
    //                     // layout object (like a group), that maintains a persistant, selectable oval
    //                     // that can be resized directly -- the bounding box would only be used for
    //                     // picking the initial size.

    //                 }
    //             }

    //     };

    public static abstract class ClusterAction extends ArrangeAction {

        boolean supportsSingleMover() {
            return false;
        }

        boolean enabledFor(LWSelection s) {
            return s.size() > 0;
        }

        ClusterAction(String labelKey, KeyStroke stroke) {
            super(VueResources.local(labelKey), stroke);
        }

        public abstract void doClusterAction(LWComponent center, Collection<LWComponent> nodes);

        void arrange(LWSelection selection) {

            final double radiusWide, radiusTall;

            selection.resetStatistics(); // todo: why do we need to reset? is this a clone? (has no statistics)

            final int nDataValues = selection.getDataValueCount();
            final int nDataRows = selection.getDataRowCount();

            if (DEBUG.Enabled) {
                Log.debug("DATAVALUECOUNT: " + nDataValues);
                Log.debug("DATA-ROW-COUNT: " + nDataRows);
            }

            if (selection.size() == 1) {

                // if a single item in selection, arrange all nodes linked to it in a circle around it

                final LWComponent center = selection.first();
                final Collection<LWComponent> linked = center.getClustered();

                final List<LWComponent> toReparent = new ArrayList();

                for (LWComponent c : linked) {
                    if (c.hasAncestor(center))
                        toReparent.add(c);
                }

                if (toReparent.size() > 0)
                    center.getParent().addChildren(toReparent, LWComponent.ADD_CHILD_TO_SIBLING);

                doClusterAction(center, linked);

                selection().setTo(center);
                selection().add(linked);

            }
            // TODO: also handle the case when all values are rows (only from
            // the same schema?) useful when joining data-sets -- the row
            // itself may be clustering related nodes from another data-set

            //else if (nDataValues == selection.size() || nDataRows == selection.size()) {
            // problem: if we're just dealing with regular non-data nodes, we won't detect....
            // So if there are no links between anything in the selection, also presume
            // we just want to do the cluster action, tho really this only applies to
            // MakeDataLists and may not apply to the other actions....

            else if (nDataValues == selection.size() || nDataRows == selection.size()) {

                // If all the items in the selection are single enumerated data
                // VALUES, (e.g., they were all selected by a single click on a
                // field in the DataTree, selecting all values for that field) then
                // perform a cluster operation on each value separately, clustering
                // all connected rows/nodes around each value.  Unless there are
                // absolutely no links involved, in which case just circle them.

                // TODO: NOT ALWAYS WHAT'S WANTED: may have a central value node (e.g.,
                // Genre=Folk), surrounded by other value nodes (e.g., Artist),
                // connected by COUNT links.  If we see count links try something
                // different.  In the meantime, this case is also causing a stack
                // overflow, as a result of clustering on all of the nodes.

                boolean anyLinks = false;
                for (LWComponent c : selection) {
                    if (c.hasLinks()) {
                        anyLinks = true;
                        break;
                    }
                }

                // TODO: for each in selection, count INTRA-SELECTION links -- if all have one,
                // and one has all, use the one with all as the CENTER

                if (anyLinks) {
                    for (LWComponent asCenter : selection) {
                        Collection<LWComponent> outGroupLinked = new ArrayList(asCenter.getClustered());
                        outGroupLinked.removeAll(selection);
                        if (DEBUG.Enabled)
                            Log.debug("asCenter: " + asCenter + "; outGroupLinked=" + Util.tags(outGroupLinked));
                        doClusterAction(asCenter, outGroupLinked);
                    }
                } else {
                    clusterNodes(selection);
                }

            }
            //else if (nDataValues == 1 && nDataRows == (selection.size() - 1)) {
            else if (nDataValues == 1) {

                // If there's a single data VALUE in the selected, and everything
                // ELSE is a data ROW, assume we really want to just do a clustering
                // around the single data-value.  This is quite a leap to make
                // given that the rows could be completely unrelated, but it's
                // the most common use case at the moment.

                // A more sane approach would be to extract the one value node,
                // and do an arrange just with all other nodes found, and not
                // care if they're data-nodes or linked nodes or not -- as long
                // as we don't do anything nutty like arrange value nodes around
                // each other, this should be fine.

                Log.debug("guessing at an all-related data-values selection");

                // find the one data value and cluster the rest around it

                LWComponent center = null;

                List<LWComponent> toCluster = new ArrayList(selection.size());

                for (LWComponent c : selection) {
                    if (c.isDataValueNode()) {
                        center = c;
                    } else {
                        toCluster.add(c);
                    }
                }

                doClusterAction(center, toCluster);
                //doClusterAction(center, center.getLinked());

            } else {

                //                     radiusWide = (maxX - minX) / 2;
                //                     radiusTall = (maxY - minY) / 2;

                //                     radiusWide = Math.max((maxX - minX) / 2, maxWide);
                //                     radiusTall = Math.max((maxY - minY) / 2, maxTall);

                radiusWide = Math.max((maxX - minX) / 2, totalWidth / 4);
                radiusTall = Math.max((maxY - minY) / 2, totalHeight / 4);

                //clusterNodes(centerX, centerY, radiusWide, radiusTall, selection);
                clusterNodes(selection);

                // The ring will expand on subsequent MakeCircle calls, because nodes are laid
                // out on the ring on-center, but the bounds used to create the initial ring
                // form the the top of the north-most mode to the bottom of the south-most node
                // (same for east/west), which on the next call will be a bigger ring.  Would
                // be hairy trying to figure out the the ring size that would contain the given
                // nodes inside a given rectangle when laid-out on-center. [ Actually, would
                // just computing the on-center bounds work? Better, but only perfectly if
                // there's a node at exaclty N/S/E/W on the dial, and the ring-order (currently
                // selection order, which is usually stacking order) hasn't changed.] If we
                // want such functionality, would be better handled via a persistent "ring"
                // layout object (like a group), that maintains a persistant, selectable oval
                // that can be resized directly -- the bounding box would only be used for
                // picking the initial size.

            }
        }

    };

    public static final ClusterAction MakeCluster = new ClusterAction("menu.format.layout.makecluster",
            keyStroke(KeyEvent.VK_PERIOD, ALT)) {
        @Override
        public void doClusterAction(LWComponent center, Collection<LWComponent> nodes) {
            clusterNodesAbout(center, nodes);
        }
    };

    public static final ClusterAction MakeDataLists = new ClusterAction("menu.format.layout.makedatalists",
            keyStroke(KeyEvent.VK_COMMA, ALT)) {

        // TODO: disabling this for multi-seletion breaks one of
        // the main great use cases for this action: whats the
        // issue we're addressing here?
        // @Override boolean enabledFor(LWSelection s) { return s.size() == 1 && s.first().hasLinks(); }

        public void doClusterAction(LWComponent c, Collection<LWComponent> nodes) {
            if (c instanceof LWNode) {
                // grab linked
                //c.addChildren(new ArrayList(c.getLinked()), LWComponent.ADD_MERGE);
                c.addChildren(nodes, LWComponent.ADD_MERGE);
            }
        }
    };

    //     public static final LWCAction MakeDataLists = new ArrangeAction(VueResources.local("menu.format.align.makedatalists"), keyStroke(KeyEvent.VK_COMMA, ALT)) {
    //             boolean enabledFor(LWSelection s) { return s.size() == 1 && s.first().hasLinks(); }
    //             // if we want this to be do-what-i-mean smart like MakeClusters, factor out
    //             // the code there the identifies the single value node v.s. all the linked data nodes,
    //             // and re-use it here for the same purpose. (That way you could easily swap back
    //             // and forth between clustered and listed displays while all the effected nodes stay selected).
    //             // ALSO, want to re-use the code to do a separate arrange when just a bunch of values are selected.
    //             @Override
    //             public void arrange(LWComponent c) {
    //                 if (c instanceof LWNode) {
    //                     // grab linked
    //                     c.addChildren(new ArrayList(c.getLinked()), LWComponent.ADD_MERGE);
    //                 }
    //             }
    //         };

    //     public static final LWCAction MakeDataLinks = new LWCAction(VueResources.local("menu.format.layout.makedatalinks"), keyStroke(KeyEvent.VK_SLASH, ALT)) {
    //             boolean enabledFor(LWSelection s) { return s.size() == 1; } // just one for now
    //             Collection<? extends LWComponent> linkTargets = null;
    // 
    //             @Override
    //             public void act(LWSelection s) {
    //                 
    //                 //tufts.vue.ds.DataAction.addDataLinksForNodes(getMap(), s, linkTargets);
    //             }
    // //             @Override
    // //             public void act(LWSelection s) {
    // //                 // we re-use linkTargets below, so we don't need to re-build the list for every node in the selection
    // //                 linkTargets = tufts.vue.ds.DataAction.getLinkTargets(s.first().getMap());
    // //                 super.act(s);
    // //             }
    // //             @Override
    // //             public void act(LWNode c) {
    // //                 tufts.vue.ds.DataAction.addDataLinksForNode(c, linkTargets);
    // // //                 tufts.vue.ds.DataAction.addDataLinksForNodes(c.getMap(),
    // // //                                                              java.util.Collections.singletonList(c),
    // // //                                                              c.getDataValueField());
    //                     
    // //             }
    //         };

    public static final ArrangeAction MakeRow = new ArrangeAction(VueResources.local("menu.format.arrange.makerow"),
            keyStroke(KeyEvent.VK_1, ALT)) {
        boolean supportsSingleMover() {
            return false;
        }

        boolean enabledFor(LWSelection s) {
            return s.size() >= 2;
        }
        // todo bug: an already made row is shifting everything to the left
        // (probably always, actually)

        void arrange(LWSelection selection) {
            AlignCentersRow.arrange(selection);
            AlignCentersRow.oldCenterX = AlignCentersRow.centerX;
            AlignCentersRow.oldCenterY = AlignCentersRow.centerY;
            maxX = minX + totalWidth;
            DistributeHorizontally.arrange(selection);
            // note that we need to check the global selection, not the passed in selection,
            // as the passed in selection for arrange actions have links filtered out.
            if (VUE.getSelection().size() == viewer().getMap().getAllDescendents(LWContainer.ChildKind.EDITABLE)
                    .size()) {
                // Would this feature be better be served by a general LWCAction flag that says
                // at the end of the action, make sure the entire selection is visible on the
                // map?  We could do a zoom-fit to the bounds of everything currently visible
                // on the map, plus everything in the current selection.  This covers both
                // cases of partial map selection and full-map selection.  We could skip
                // the zoom fit entirely if the current selection is already fully visible.
                ZoomTool.setZoomOutFit();
            }
        }
    };

    public static final ArrangeAction MakeColumn = new ArrangeAction(
            VueResources.local("menu.format.arrange.makecolumn"), keyStroke(KeyEvent.VK_2, ALT)) {
        boolean supportsSingleMover() {
            return false;
        }

        boolean enabledFor(LWSelection s) {
            return s.size() >= 2;
        }

        void arrange(LWSelection selection) {
            AlignCentersColumn.arrange(selection);
            //                 float height;
            //                 if (selection.getHeight() > 0)
            //                     height = selection.getHeight();
            //                 else
            //                     height = totalHeight;
            //                 maxY = minY + height;
            maxY = minY + totalHeight;
            DistributeVertically.arrange(selection);
            //Log.debug("   VUE-SELECTION: " + VUE.getSelection());
            //Log.debug("ACTION-SELECTION: " + selection);
            // note that we need to check the global selection, not the passed in selection,
            // as the passed in selection for arrange actions have links filtered out.
            if (VUE.getSelection().size() == viewer().getMap().getAllDescendents(LWContainer.ChildKind.EDITABLE)
                    .size()) {
                ZoomTool.setZoomOutFit();
            }
        }
    };

    public static final ArrangeAction DistributeVertically = new ArrangeAction(
            VueResources.local("menu.format.arrange.distributevertically"), keyStroke(KeyEvent.VK_V, ALT)) {
        boolean supportsSingleMover() {
            return false;
        }

        boolean enabledFor(LWSelection s) {
            return s.size() >= 3;
        }

        // use only *2* in selection if use our minimum layout region setting
        void arrange(LWSelection selection) {
            LWComponent[] comps = sortByY(sortByX(selection.asArray()));
            float layoutRegion = maxY - minY;
            //if (layoutRegion < totalHeight)
            //  layoutRegion = totalHeight;
            float verticalGap = (layoutRegion - totalHeight) / (selection.size() - 1);
            float y;
            if (Float.isNaN(oldCenterY)) {
                y = minY;
            } else {
                y = oldCenterY - layoutRegion / 2;
            }
            for (int i = 0; i < comps.length; i++) {
                LWComponent c = comps[i];
                c.setLocation(c.getX(), y);
                y += c.getHeight() + verticalGap;
            }
        }
    };

    public static final ArrangeAction DistributeHorizontally = new ArrangeAction(
            VueResources.local("menu.format.arrange.distributehorizontally"), keyStroke(KeyEvent.VK_H, ALT)) {
        boolean supportsSingleMover() {
            return false;
        }

        boolean enabledFor(LWSelection s) {
            return s.size() >= 3;
        }

        void arrange(LWSelection selection) {
            final LWComponent[] comps = sortByX(sortByY(selection.asArray()));
            final float layoutRegion = maxX - minX;
            //if (layoutRegion < totalWidth)
            //  layoutRegion = totalWidth;
            final float horizontalGap = (layoutRegion - totalWidth) / (selection.size() - 1);
            float x;
            if (Float.isNaN(oldCenterX)) {
                x = minX;
            } else {
                x = oldCenterX - layoutRegion / 2;
            }
            for (LWComponent c : comps) {
                c.setLocation(x, c.getY());
                x += c.getWidth() + horizontalGap;
            }
        }

    };

    /** Helpers for menu creation.  Null's indicate good places
     * for menu separators. */

    public static final Action[] ALIGN_MENU_ACTIONS = { AlignLeftEdges, AlignRightEdges, AlignTopEdges,
            AlignBottomEdges, null, AlignCentersRow, AlignCentersColumn, null, FillWidth, FillHeight };

    public static final Action[] ARRANGE_MENU_ACTIONS = { MakeRow, MakeColumn, null, LayoutAction.table,
            LayoutAction.circle, LayoutAction.filledCircle, LayoutAction.random, LayoutAction.ripple,
            LayoutAction.cluster2, null, PushOut, PullIn,
            //PushOutLinked,
            null, DistributeVertically, DistributeHorizontally, null, BringToFront, SendToBack };

    public static final LWCAction ImageToNaturalSize = new LWCAction(VueResources.local("action.makenaturalsize")) {
        @Override
        boolean enabledFor(LWSelection s) {
            return s.containsType(LWImage.class) || s.containsType(LWNode.class); // todo: really, only image nodes, but we have no key for that
        }

        public void act(LWImage c) {
            c.setToNaturalSize();
        }

        public void act(LWNode n) {
            LWImage i = n.getImage();
            if (i != null)
                i.setToNaturalSize();

        }
    };

    private static class ImageSizeAction extends LWCAction {
        final int size;

        ImageSizeAction(String name) {
            super(name);
            this.size = -1;
        }

        ImageSizeAction(String name, KeyStroke shortcut) {
            super(name, shortcut);
            this.size = -1;
        }

        ImageSizeAction(int size) {
            super(size + "x" + size);
            this.size = size;
        }

        @Override
        boolean enabledFor(LWSelection s) {
            return s.containsType(LWImage.class) || s.containsType(LWNode.class); // todo: really, only image nodes, but we have no key for that
        }

        protected void imageAct(LWImage im, Object actionKey) {
            final int newDim;

            //Log.debug(this + " on " + im);

            if (actionKey == IMAGE_SHOW) {
                if (im.isHidden(HideCause.IMAGE_ICON_OFF)) {
                    im.clearHidden(HideCause.IMAGE_ICON_OFF);
                    im.getParent().layout("imageIconShow");
                }
                return;
            }

            if (actionKey == IMAGE_HIDE)
                newDim = Integer.MIN_VALUE;
            else if (actionKey == IMAGE_BIGGER)
                newDim = getBiggerSize(im); // will return same size if is currently OFF
            else if (actionKey == IMAGE_SMALLER)
                newDim = getSmallerSize(im);
            else // actionKey is an Integer representing the new desired size
                newDim = (Integer) actionKey;

            if (DEBUG.IMAGE)
                Log.debug("NEWDIM " + newDim);

            if (newDim == Integer.MIN_VALUE) {
                // hide
                if (im.isNodeIcon() || im.getParent() instanceof LWNode) {
                    im.setHidden(HideCause.IMAGE_ICON_OFF);
                    im.getParent().layout("imageIconHide");
                }
            } else if (newDim == Integer.MAX_VALUE) {
                // make natural size
                im.setToNaturalSize();
                if (im.isNodeIcon()) {
                    im.clearHidden(HideCause.IMAGE_ICON_OFF);
                    im.getParent().layout("imageIconShow");
                }
            } else {
                // adjust size
                im.setMaxDimension(newDim);
                if (im.isNodeIcon()) {
                    im.clearHidden(HideCause.IMAGE_ICON_OFF);
                    im.getParent().layout("imageIconShow");
                }
            }
        }

        @Override
        public void act(LWImage im) {
            imageAct(im, size);
        }

        @Override
        public void act(LWNode n) {
            final LWImage image = n.getImage();
            if (image != null)
                act(image);
        }
    }

    private static final Object IMAGE_BIGGER = "bigger";
    private static final Object IMAGE_SMALLER = "smaller";
    private static final Object IMAGE_HIDE = "hide";
    private static final Object IMAGE_SHOW = "show";

    private static final class ImageAdjustAction extends ImageSizeAction {
        final Object actionKey;

        ImageAdjustAction(String localizationKey, Object key) {
            super(VueResources.local(localizationKey));
            this.actionKey = key;
        }

        ImageAdjustAction(String localizationKey, Object key, KeyStroke shortcut) {
            super(VueResources.local(localizationKey), shortcut);
            this.actionKey = key;
        }

        @Override
        public void act(LWImage im) {
            imageAct(im, actionKey);
        }
    }

    private static final LWCAction ImageBigger = new ImageAdjustAction("action.image.bigger", IMAGE_BIGGER,
            keyStroke(KeyEvent.VK_CLOSE_BRACKET, COMMAND + SHIFT));
    private static final LWCAction ImageSmaller = new ImageAdjustAction("action.image.smaller", IMAGE_SMALLER,
            keyStroke(KeyEvent.VK_OPEN_BRACKET, COMMAND + SHIFT));
    private static final LWCAction ImageHide = new ImageAdjustAction("action.image.hide", IMAGE_HIDE);
    private static final LWCAction ImageShow = new ImageAdjustAction("action.image.show", IMAGE_SHOW);

    private static final int ImageSizes[] = { 1024, 768, 640, 512, 384, 256, 128, 64, 32, 16 };

    public static final Action[] IMAGE_MENU_ACTIONS;
    public static final Action[] NODE_FORMAT_MENU_ACTIONS = { ResizeNode };

    static {

        IMAGE_MENU_ACTIONS = new Action[ImageSizes.length + 5];

        int i = 0;

        IMAGE_MENU_ACTIONS[i++] = ImageBigger;
        IMAGE_MENU_ACTIONS[i++] = ImageSmaller;
        IMAGE_MENU_ACTIONS[i++] = ImageToNaturalSize;

        for (int x = 0; x < ImageSizes.length; x++) {
            IMAGE_MENU_ACTIONS[i++] = new ImageSizeAction(ImageSizes[x]);
        }

        IMAGE_MENU_ACTIONS[i++] = ImageHide;
        IMAGE_MENU_ACTIONS[i++] = ImageShow;

    }

    /** @return the next biggest size, unless the image icon is currently hidden, in which case return same size */
    private static int getBiggerSize(LWImage c) {
        final int maxDim = (int) Math.max(c.getWidth(), c.getHeight());

        if (c.isHidden(HideCause.IMAGE_ICON_OFF))
            return maxDim;

        //Log.debug("BIGGER MAXDIM " + maxDim);

        for (int i = ImageSizes.length - 1; i >= 0; i--) {
            if (ImageSizes[i] > maxDim)
                return ImageSizes[i];
        }
        return Integer.MAX_VALUE;
    }

    private static int getSmallerSize(LWImage c) {
        final int maxDim = (int) Math.max(c.getWidth(), c.getHeight());

        //Log.debug("SMALLER MAXDIM " + maxDim);

        for (int i = 0; i < ImageSizes.length; i++) {
            if (ImageSizes[i] < maxDim)
                return ImageSizes[i];
        }

        return ImageSizes[ImageSizes.length - 1];
        //return Integer.MIN_VALUE; // will hide the image instead of going to smallest
    }

    //-----------------------------------------------------------------------------
    // VueActions
    //-----------------------------------------------------------------------------
    public static final Action GatherWindows = new VueAction(VueResources.local("menu.windows.gather")) {
        boolean undoable() {
            return false;
        }

        protected boolean enabled() {
            return true;
        }

        public void act() {
            GUI.reloadGraphicsInfo();
            GUI.invokeAfterAWT(new Runnable() {
                public void run() {
                    DockWindow acrossTop[] = new DockWindow[VUE.acrossTop.length];
                    System.arraycopy(VUE.acrossTop, 0, acrossTop, 0, VUE.acrossTop.length);
                    //acrossTop[VUE.acrossTop.length] = VUE.getMergeMapsDock();
                    //acrossTop[VUE.acrossTop.length+1] = VUE.getFormatDock();
                    //acrossTop[VUE.acrossTop.length+1] = VUE.getInteractionToolsDock();
                    VUE.getFormatDock().setLocation(150, 150);
                    VUE.getMergeMapsDock().setLocation(150, 150);
                    VUE.assignDefaultPositions(acrossTop);
                }
            });
        }
    };
    /** move the input focus to the main-window search box */
    public static final VueAction FocusToSearchField = new VueAction(local("dockWindow.search.title"),
            keyStroke(KeyEvent.VK_F, COMMAND)) {
        boolean undoable() {
            return false;
        }

        protected boolean enabled() {
            return true;
        }

        public void act() {
            VUE.mSearchTextField.requestFocus();
        }
    };
    /** Our standard text field behaviour is save on focus loss, so moving focus to another
     * standard location accomplishes text saving.  This is handy because many text input fields
     * allow return/enter in the string (an otherwise normally expected "done" key) and this
     * provides a away to say "done".  And having an action that always brings focus back to the
     * map is handy to make sure the selection is activated. */
    public static final VueAction FocusToViewer = new VueAction("Finish Editing Text",
            keyStroke(KeyEvent.VK_ENTER, COMMAND)) {
        boolean undoable() {
            return false;
        }

        protected boolean enabled() {
            return true;
        }

        public void act() {
            MapViewer viewer = VUE.getActiveViewer();
            if (viewer != null)
                viewer.requestFocus();
        }
    };
    public static final Action NewMap = new VueAction(VueResources.local("menu.file.new"),
            keyStroke(KeyEvent.VK_N, COMMAND + SHIFT), ":general/New") {
        private int count = 1;

        boolean undoable() {
            return false;
        }

        protected boolean enabled() {
            return true;
        }

        public void act() {
            VUE.displayMap(new LWMap(VueResources.local("vue.main.newmap") + count++));
        }
    };
    public static final Action Revert =
            //new VueAction("Revert", keyStroke(KeyEvent.VK_R, COMMAND+SHIFT), ":general/Revert") { // conflicts w/align centers in row
            //new VueAction("Revert", null, ":general/Revert") {            
            new VueAction(VueResources.local("menu.file.revert")) {
                boolean undoable() {
                    return false;
                }

                protected boolean enabled() {

                    return true;
                }

                public void act() {

                    if (tufts.vue.VUE.getActiveMap().getFile() == null) {
                        VueUtil.alert(VUE.getApplicationFrame(), VueResources.local("dialog.revert.message"),
                                VueResources.local("dialog.revert.title"), JOptionPane.PLAIN_MESSAGE);

                        return;
                    }
                    LWMap map = tufts.vue.VUE.getActiveMap();
                    VUE.closeMap(map, true);
                    tufts.vue.action.OpenAction.reloadMap(map);

                }
            };
    public static final Action CloseMap = new VueAction(VueResources.local("menu.file.close"),
            keyStroke(KeyEvent.VK_W, COMMAND)) {
        // todo: listen to map viewer display event to tag
        // with currently displayed map name
        boolean undoable() {
            return false;
        }

        public void act() {
            VUE.closeMap(VUE.getActiveMap());
        }
    };
    public static final Action Undo = new VueAction(VueResources.local("action.undo"),
            keyStroke(KeyEvent.VK_Z, COMMAND), ":general/Undo") {
        boolean undoable() {
            return false;
        }

        public void act() {
            VUE.getUndoManager().undo();
        }

    };
    public static final Action Redo = new VueAction(VueResources.local("action.redo"),
            keyStroke(KeyEvent.VK_Z, COMMAND + SHIFT), ":general/Redo") {
        boolean undoable() {
            return false;
        }

        public void act() {
            VUE.getUndoManager().redo();
        }
    };

    //-------------------------------------------------------
    // Zoom actions
    // Consider having the ZoomTool own these actions -- any
    // other way to have mutiple key values trigger an action?
    // Something about this feels kludgy.
    //-------------------------------------------------------

    public static final VueAction ZoomIn =
            //new VueAction("Zoom In", keyStroke(KeyEvent.VK_PLUS, COMMAND)) {
            new VueAction(VueResources.local("menu.view.zoomin"), keyStroke(KeyEvent.VK_EQUALS, COMMAND),
                    ":general/ZoomIn") {
                public void act() {
                    ZoomTool.setZoomBigger(null);
                }
            };
    public static final VueAction ZoomOut = new VueAction(VueResources.local("menu.view.zoomout"),
            keyStroke(KeyEvent.VK_MINUS, COMMAND), ":general/ZoomOut") {
        public void act() {
            ZoomTool.setZoomSmaller(null);
        }
    };
    public static final VueAction ZoomFit = new VueAction(VueResources.local("menu.view.fitinwin"),
            keyStroke(KeyEvent.VK_CLOSE_BRACKET, COMMAND), ":general/Zoom") {
        public void act() {
            ZoomTool.setZoomFit();
        }
    };
    public static final VueAction ZoomActual = new VueAction(VueResources.local("actions.zoomActual.label"),
            keyStroke(KeyEvent.VK_QUOTE, COMMAND)) {
        // no way to listen for zoom change events to keep this current
        //boolean enabled() { return VUE.getActiveViewer().getZoomFactor() != 1.0; }
        public void act() {
            ZoomTool.setZoom(1.0);
        }
    };

    public static final Action ZoomToSelection = new LWCAction(VueResources.local("menu.view.selecfitwin"),
            keyStroke(KeyEvent.VK_OPEN_BRACKET, COMMAND)) {
        public void act(LWSelection s) {
            MapViewer viewer = VUE.getActiveViewer();
            ZoomTool.setZoomFitRegion(viewer, s.getBounds(), 16, false);
        }
    };

    public static final VueAction SuperScreen = new VueAction("Use all screens for Full Screen") {
        private final Object INIT = "init";
        private boolean selected;

        private final boolean ViswallMode;

        //--------------------------------------------
        // anonymous constructor init:
        {
            boolean foundViswall = false;

            try {
                foundViswall = checkForViswall();
            } catch (Throwable t) {
                Log.info("checking for viswall", t);
            }

            ViswallMode = foundViswall;
            if (!foundViswall)
                update(INIT);
        }
        //--------------------------------------------

        boolean checkForViswall() {
            String host = System.getenv("HOST");
            if (host == null)
                host = System.getenv("HOSTNAME");
            if (host == null)
                host = System.getenv("COMPUTERNAME");
            if (host == null)
                host = System.getenv("USERDOMAIN");

            Rectangle specialBounds = null;

            if (false) { // testing
                specialBounds = new Rectangle(128, 128, 640, 480);
            } else if ("VISWALL-WIN32".equalsIgnoreCase(host) && tufts.vue.gui.Screen.getAllScreens().length == 9) {
                // TODO: The below configuration(s) need testing and may need adjusting:
                // specialBounds = new Rectangle(1920,-1080, 4096,2160); // upper logical stero display
                specialBounds = new Rectangle(1920, 0, 4096, 2160); // lower logical stero display
            }
            //else if ("insert-viswall-linux-hostname" etc..
            // // config linux viswall bounds...
            //}

            if (specialBounds != null) {
                // manually init the action, as that will (must) be skipped when we return true:
                GUI.setSpecialWorkingBounds(specialBounds);
                setEnabled(true);
                setActionName("Enable Tufts VISWALL");
                return true;
            } else {
                return false;
            }
        }

        boolean undoable() {
            return false;
        }

        protected boolean enabled() {
            return true;
        }

        public void act() {
            GUI.reloadGraphicsInfo();
            update("firing");
            if (isEnabled()) {

                selected = !selected; // this line changes behavior of GUI.setFullScreen

                if (VUE.inWorkingFullScreen() && !VUE.inNativeFullScreen()) {
                    tufts.vue.gui.GUI.setFullScreen(GUI.getFullScreenWindow());
                }

            } else {
                selected = false;
            }
        }

        @Override
        public Boolean getToggleState() {
            return selected ? Boolean.TRUE : Boolean.FALSE;
        }

        // for GUI.java -- would be better as a listener
        @Override
        public void update(Object key) {
            if (DEBUG.Enabled)
                Log.debug("SuperScreen update: " + Util.tags(key));
            if (ViswallMode)
                return;
            if (GUI.hasMultipleScreens()) {
                java.awt.Rectangle b = GUI.getAllScreenBounds();
                setActionName(String.format("All Screens (%dx%d)", b.width, b.height));
                setEnabled(true);
            } else {
                setEnabled(false);
                setActionName("All Screens");
            }
        }
    };

    public static final VueAction KioskScreen = new VueAction(VueResources.getString("kiosk.action")) {
        private final Object INIT = "init";
        private boolean selected;

        boolean undoable() {
            return false;
        }

        protected boolean enabled() {
            return true;
        }

        KioskThread kt = null;//
        Thread t = null;// new Thread(kt);

        public void act() {
            if (t == null) {
                VUE.toggleFullScreen(false, true);
                kt = new KioskThread();

                t = new Thread(kt);
                t.setPriority(Thread.MAX_PRIORITY);
                t.start();
            } else {
                VUE.toggleFullScreen(false, true);
                kt.done();
                t = null;

            }

        }

        @Override
        public Boolean getToggleState() {
            return selected ? Boolean.TRUE : Boolean.FALSE;
        }

        class KioskThread implements Runnable {
            private boolean done = false;

            public void done() {
                done = true;
            }

            public void run() {

                int dx = 0;
                MapViewer mv = FullScreen.getLastActive();
                MapViewer mv2 = VUE.getActiveViewer();
                ZoomTool.setZoom(mv, mv2.getZoomFactor());
                LWMap map = VUE.getActiveMap();
                double maxX;
                double minX;
                int mvWidth;

                while (true) {
                    if (done)
                        return;

                    maxX = mv.getVisibleBounds().getMaxX();
                    minX = mv.getVisibleBounds().getMinX();
                    mvWidth = mv.getWidth();

                    //                   System.out.println("Visible Bounds Max X:" + mv.getVisibleBounds().getMaxX() + " ::: " + mv.getWidth() + " :::" + mv.getVisibleBounds().getMinX());
                    if (maxX < mvWidth) {
                        mv.panScrollRegion((int) dx, (int) 0, false);
                        mv2.panScrollRegion((int) dx, (int) 0, false);
                    }

                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block

                    }

                    dx = 1;
                }

            }
        }
    };

    public static final VueAction ToggleFullScreen = new VueAction(VueResources.local("menu.view.fullscreen"),
            keyStroke(KeyEvent.VK_BACK_SLASH, COMMAND)) {
        public void act() {
            if (PresentationTool.ResumeActionName.equals(getActionName())) {
                PresentationTool.ResumePresentation();
                revertActionName(); // go back to original action
            } else {
                VUE.toggleFullScreen(false, true);

            }
        }

        @Override
        public Boolean getToggleState() {
            return tufts.vue.gui.FullScreen.inFullScreen();
        }

        public boolean overrideIgnoreAllActions() {
            return true;
        }

    };

    public static final VueAction ToggleSlideIcons = new VueAction(VueResources.local("menu.view.slidethumbnails"),
            keyStroke(KeyEvent.VK_T, SHIFT + COMMAND)) {
        public void act() {
            LWPathway.toggleSlideIcons();
            PathwayPanel.getInstance().updateShowSlidesButton();

            // This won't do anything if something deeper in the map is the focal
            //VUE.getActiveMap().notify(this, LWKey.Repaint);

            VUE.getActiveFocal().notify(this, LWKey.Repaint);

            //                 if (VUE.getActivePathway() != null) {
            //                     //VUE.getActivePathway().notify("pathway.showSlides");
            //                     VUE.getActivePathway().notify(this, LWKey.Repaint);
            //                 } else {
            //                     VUE.getActiveMap().notify(this, LWKey.Repaint);
            //                 }
        }

        @Override
        public Boolean getToggleState() {
            return LWPathway.isShowingSlideIcons();
        }

        public boolean overrideIgnoreAllActions() {
            return true;
        }
    };

    public static final Action ToggleSplitScreen = new VueAction(VueResources.local("menu.view.splitscreen"),
            keyStroke(KeyEvent.VK_BACK_SLASH, COMMAND + SHIFT)) {
        boolean state;

        public void act() {
            // todo: doesn't work (see VUE.java)
            state = VUE.toggleSplitScreen();
        }

        @Override
        public Boolean getToggleState() {
            return state;
        }

        public boolean overrideIgnoreAllActions() {
            return true;
        }
    };

    public static final Action ToggleLinks = new VueAction(VueResources.local("menu.view.hideLinks"),
            keyStroke(KeyEvent.VK_L, CTRL_ALT)) {
        public void act() {
            Actions.toggleLinkVisiblity();
        }

        public Boolean getToggleState() {
            return areLinksFiltered();
        }
    };

    /*
     * I think because of the way this is proposed to work
     * we can't maintain a static state we have to always calculate the state
     * based on the selection. -MK
     */
    static void toggleLinkVisiblity() {

        boolean filtered = areLinksFiltered();
        LWSelection s = VUE.getSelection();
        if (s.size() > 0) {
            Iterator it = s.iterator();
            for (LWComponent c : s) {
                if (c instanceof LWLink) {
                    ((LWLink) c).setFiltered(!filtered);
                }
            }
        } else
            for (LWComponent c : VUE.getActiveViewer().getMap().getAllDescendents()) {
                if (c instanceof LWLink)
                    ((LWLink) c).setFiltered(!filtered);
            }
        VUE.getActiveViewer().repaint();
    }

    static Boolean areLinksFiltered() {
        LWSelection s = VUE.getSelection();
        if (s.size() > 0) {
            Iterator it = s.iterator();
            for (LWComponent c : s) {
                if (c instanceof LWLink) {
                    boolean isFiltered = ((LWLink) c).isFiltered();

                    if (isFiltered)
                        return true;
                }
            }
            return false;
        } else
            for (LWComponent c : VUE.getActiveViewer().getMap().getAllDescendents()) {
                if (c instanceof LWLink) {
                    boolean isFiltered = ((LWLink) c).isFiltered();

                    if (isFiltered)
                        return true;
                }
            }
        return false;
    }

    public static final VueAction TogglePruning = new VueAction(VueResources.local("menu.view.pruning"),
            keyStroke(KeyEvent.VK_J, COMMAND)) {
        public void act() {
            final boolean wasEnabled = togglePruningEnabled();

            // Currently, this action is ONLY fired via a menu item.  If other code points might
            // set this directly (the global pruning state), this should be changed to a
            // toggleState action (impl getToggleState), and those code points should call this
            // action to do the toggle, so the menu item checkbox state will stay synced.

            VUE.layoutAllMaps(HideCause.PRUNE);
            viewer().repaint();

            //             if (wasEnabled) {
            //                 // turning off pruning
            //                 for (LWMap map : VUE.getAllMaps()) {
            //                     for (LWComponent c : map.getAllDescendents()) {
            //                         c.clearHidden(HideCause.PRUNE);
            //                         if (c instanceof LWLink)
            //                             ((LWLink)c).clearPrunes();
            //                     }
            //                 }
            //                 VUE.layoutAllMaps(HideCause.PRUNE);
            //             } else {
            //                 // turning on pruning -- show prune controls on any selected links
            //                 viewer().repaint();
            //             }
        }
    };

    private static boolean togglePruningEnabled() {
        final boolean wasEnabled = LWLink.isPruningEnabled();

        LWLink.setPruningEnabled(!wasEnabled);

        setAllPruneHidesEnabled(!wasEnabled);

        return wasEnabled;
    }

    public static final VueAction ClearAllPruning = new VueAction(VueResources.local("menu.view.clearpruning")) {
        public void act() {
            clearAllPruneStates(viewer().getMap());
            viewer().repaint();
        }
    };

    /** erase all pruning state from the given map */
    private static void clearAllPruneStates(LWMap map) {
        for (LWComponent c : map.getAllDescendents()) {
            c.setPruned(false);
            c.clearHidden(HideCause.PRUNE);
            if (c instanceof LWLink)
                ((LWLink) c).clearUserPrunes();
        }
    }

    private static void setAllPruneHidesEnabled(final boolean enable) {
        for (LWMap map : VUE.getAllMaps()) {
            for (LWComponent c : map.getAllDescendents()) {
                if (c.isPruned())
                    c.setHidden(HideCause.PRUNE, enable);
            }
        }
    }

    public static final VueAction ToggleLinkLabels = new VueAction("Link Labels") {
        public void act() {
            boolean enabled = LWLink.isDisplayLabelsEnabled();

            // Currently, this action is ONLY fired via a menu item.  If other code
            // points might set this directly, this should be changed to a toggleState
            // action (impl getToggleState), and those code points should call this
            // action to do the toggle, so the menu item checkbox state will stay
            // synced.

            LWLink.setDisplayLabelsEnabled(!enabled);

            VUE.getActiveMap().notify(this, LWKey.Repaint);
        }
    };

    public static final VueAction ToggleAutoZoom =

            // 'E' chosen for temporary mac shortcut until we find a workaround for not
            // being able to use Alt-Z because it's on the left of the keyboard, and it's
            // not 'W', which if the user accidently hits COMMAND-W, the map will close
            // (todo: see about just changing the Close shortcut entirely or getting rid of
            // it)

            new VueAction(VueResources.local("menu.format.autozoom"), keyStroke(KeyEvent.VK_E, COMMAND + SHIFT)) {
                boolean state = edu.tufts.vue.preferences.implementations.AutoZoomPreference.getInstance().isTrue();
                {
                    updateName();
                }

                @Override
                public void act() {
                    state = !state;
                    updateName();
                    edu.tufts.vue.preferences.implementations.AutoZoomPreference.getInstance()
                            .setValue(Boolean.valueOf(state));
                }

                void updateName() {
                    if (DEBUG.Enabled && DEBUG.KEYS && Util.isMacPlatform()) {
                        // workaroud for mac java bug with accelerator glpyhs in JCheckBoxMenuItem's
                        if (state)
                            putValue(NAME, getPermanentActionName() + " (ON)");
                        else
                            putValue(NAME, getPermanentActionName() + " (off)");
                    }
                }

                @Override
                public Boolean getToggleState() {
                    return state;
                }

                public boolean overrideIgnoreAllActions() {
                    return true;
                }
            };

    public static final LWCAction NewSlide = new LWCAction(VueResources.local("actions.newSlide.label")) {
        public void act(Iterator i) {
            VUE.getActivePathway().add(i);
            GUI.makeVisibleOnScreen(VUE.getActiveViewer(), PathwayPanel.class);

        }

        boolean enabledFor(LWSelection s) {
            // items can be added to pathway as many times as you want
            return VUE.getActivePathway() != null && s.size() > 0;
        }
    };

    public static final LWCAction MergeNodeSlide = new LWCAction(VueResources.local("actions.mergeNode.label")) {
        public void act(Iterator i) {
            final LWComponent node = VUE.getActivePathway().createMergedNode(VUE.getSelection());
            node.setLocation(VUE.getActiveViewer().getLastMousePressMapPoint());
            VUE.getActiveViewer().getMap().add(node);
            VUE.getActivePathway().add(node);
        }

        boolean enabledFor(LWSelection s) {
            // items can be added to pathway as many times as you want
            return VUE.getActivePathway() != null && s.size() > 0;
        }
    };

    public static final VueAction NewNode = new NewItemAction(VueResources.local("menu.content.addnode"),
            keyStroke(KeyEvent.VK_N, COMMAND)) {
        @Override
        LWComponent createNewItem() {
            return NodeModeTool.createNewNode();
        }
    };

    //This doesn't really make a lot of sense to have 2 methods do the
    //same thing but my MapViewer.java is a bit decomposed at the moment so
    //TODO: Come back here eliminate one of these and only call one from mapviewer.
    //MK
    public static final VueAction NewRichText = new NewItemAction(VueResources.local("menu.content.addtext"),
            keyStroke(KeyEvent.VK_T, COMMAND)) {
        @Override
        LWComponent createNewItem() {
            return NodeModeTool.createRichTextNode(VueResources.local("newtext.html"));
        }
    };

    public static final Action[] NEW_OBJECT_ACTIONS = { NewNode, NewRichText,
            //AddImageAction,
            //AddFileAction,
            //NewSlide
    };

    static class NewItemAction extends VueAction {
        static LWComponent lastItem = null;
        static Point lastMouse = null;
        static Point2D lastLocation = null;

        NewItemAction(String name, KeyStroke keyStroke) {
            super(name, null, keyStroke, null);
        }

        /** @return true -- while there's an on-map label edit active, all
         * actions are disabled, however, we want to permit repeated
         * new-item actions, and new item actions auto-activate a label
         * edit, so we allow this even if everything is disabled */
        @Override
        public boolean overrideIgnoreAllActions() {
            return VUE.getActiveViewer() != null && VUE.getActiveTool().supportsEditActions();
        }

        public void act() {
            final MapViewer viewer = VUE.getActiveViewer();
            final Point currentMouse = viewer.getLastMousePressPoint();
            final Point2D newLocation = viewer.screenToFocalPoint(currentMouse);

            if (currentMouse.equals(lastMouse) && lastItem.getLocation().equals(lastLocation)) {
                // would it be better to just put in a column instead of staggering?
                // staggering (the x adjustment) does give them more flexibility on future
                // arrange actions tho.
                newLocation.setLocation(lastLocation.getX() + 10,
                        lastLocation.getY() + lastItem.getLocalBorderHeight());
            }

            lastItem = createNewItem(viewer, newLocation);
            lastLocation = newLocation;
            lastMouse = currentMouse;
        }

        /**
         * The default creator: add's to map at current location and activates label edit
         * if label is supported on the object -- override if want something different.
         */
        LWComponent createNewItem(final MapViewer viewer, Point2D newLocation) {
            final LWComponent newItem = createNewItem();

            newItem.setLocation(newLocation);
            //newItem.setCenterAt(newLocation); // better but screws up NewItemAction's serial item creation positioning

            // maybe: run a timer and do this if no activity (e.g., node creation)
            // for 250ms or something
            viewer.getFocal().dropChild(newItem);

            //GUI.invokeAfterAWT(new Runnable() { public void run() {
            viewer.getSelection().setTo(newItem);
            //}});

            if (newItem.supportsUserLabel()) {
                // Just in case, do this later:
                GUI.invokeAfterAWT(new Runnable() {
                    public void run() {
                        viewer.activateLabelEdit(newItem);
                    }
                });
            }

            return newItem;
        }

        LWComponent createNewItem() {
            throw new UnsupportedOperationException("NewItemAction: unimplemented create");
        }

    }

    /**
     * LWCAction: actions that operate on one or more LWComponents.
     * Provides a number of convenience methods to allow code in
     * each action to be tight & focused.
     */

    // TODO: set an activeViewer member in VueAction, so we don't have to fetch it again
    // in any of the actions, and more importantly we can know for certian it can never
    // change from null to non-null between the time we check for nulls and fetch it
    // again, tho this should in fact be "impossible"...

    public static class LWCAction extends VueAction {
        public LWCAction(String name, String shortDescription, KeyStroke keyStroke, Icon icon) {
            super(name, shortDescription, keyStroke, icon);
            init();
        }

        LWCAction(String name, KeyStroke keyStroke, String iconName) {
            super(name, keyStroke, iconName);
            init();
        }

        LWCAction(String name, String shortDescription, KeyStroke keyStroke) {
            this(name, shortDescription, keyStroke, (Icon) null);
        }

        LWCAction(String name) {
            this(name, null, null, (Icon) null);
        }

        LWCAction(String name, Icon icon) {
            this(name, null, null, icon);
        }

        LWCAction(String name, KeyStroke keyStroke) {
            this(name, null, keyStroke, (Icon) null);
        }

        public void act() {
            LWSelection selection = selection();
            //System.out.println("LWCAction: " + getActionName() + " n=" + selection.size());
            if (enabledFor(selection)) {
                if (mayModifySelection()) {
                    selection = (LWSelection) selection.clone();
                }
                act(selection);
                VUE.getActiveViewer().repaintSelection();
            } else {
                // This shouldn't happen as actions should already
                // be disabled if they're not appropriate, tho
                // if the action depends on something other than
                // the selection and isn't listening for it, we'll
                // get here.
                java.awt.Toolkit.getDefaultToolkit().beep();
                Log.error(getActionName() + ": Not enabled given this selection: " + selection);
            }
        }

        //         public void fire(InputEvent e, LWComponent c) {
        //             actionPerformed(new ActionEvent(source, 0, name));
        //         }

        ///** option initialization code called at end of constructor */
        private void init() {
            //VUE.getSelection().addListener(this);
        }

        @Override
        protected boolean isSelectionWatcher() {
            return true;
        }

        /** @return true -- the default for LWCAction's */
        @Override
        boolean isEditAction() {
            return true;
        }

        @Override
        protected boolean enabled() {
            return VUE.getActiveViewer() != null && enabledFor(selection());
        }

        //         public void selectionChanged(LWSelection selection) {
        //             if (VUE.getActiveViewer() == null)
        //                 setEnabled(false);
        //             else
        //                 setEnabled(enabledFor(selection));
        //         }
        //         void checkEnabled() {
        //             selectionChanged(VUE.getSelection());
        //         }

        void checkEnabled() {
            //selectionChanged(VUE.getSelection());
            updateEnabled(selection());
        }

        protected void updateEnabled(LWSelection selection) {
            if (selection == null)
                setEnabled(false);
            else
                setEnabled(enabledFor(selection));
        }

        /** Is this action enabled given this selection? */
        @Override
        boolean enabledFor(LWSelection s) {
            return s.size() > 0;
        }

        /** mayModifySelection: the action may result in an event that
         * has the viewer change what's in the current selection
         * (e.g., on delete, the viewer makes sure the deleted object
         * is no longer in the selection group -- we need this because
         * actions usually iterate thru the selection, and if it might
         * change in the middle of the iteration, we have to clone it
         * before going thru it or we will get conncurrent
         * modification exceptions.  An action does NOT need to
         * declare that it may modification the selection if it just
         * changes the selection at the end of the iteration (e.g., by
         * setting the selection to newly copied nodes or something)
         * */

        boolean mayModifySelection() {
            return false;
        }

        //         /** hierarchicalAction: any children in selection who's
        //          * parent is also in the selection are ignore during
        //          * iterator -- for actions such as delete where deleting
        //          * the parent will automatically delete any children.
        //          */
        //         boolean hierarchicalAction() { return false; }

        void act(LWSelection selection) {
            act(selection.iterator());
        }

        /**
         * Automatically apply the action serially to everything in the
         * selection -- override if this isn't what the action
         * needs to do.
         *
         * Note that the default is to descend into instances of LWGroup
         * and apply the action seperately to each child, and NOT
         * to apply the action to any nodes that are children of
         * other nodes. If the child is already in selection (e.g.
         * a select all was done) be sure NOT to act on it, otherwise
         * the action will be done twice). [ Why was this? -- disabled 2007-05-30 -- SMF ]
         */
        void act(Iterator<LWComponent> i) {
            while (i.hasNext()) {
                LWComponent c = i.next();
                //                 if (hierarchicalAction() && c.isAncestorSelected()) {
                //                     // If has no parent, must already have been acted on to get that way.
                //                     // If parent is selected, action will happen via it's parent.
                //                     continue;
                //                 }
                act(c);
            }
        }

        void act(LWComponent c) {
            if (c instanceof LWLink)
                act((LWLink) c);
            else if (c instanceof LWNode)
                act((LWNode) c);
            else if (c instanceof LWImage)
                act((LWImage) c);
            else if (c instanceof LWSlide)
                act((LWSlide) c);
            else if (DEBUG.SELECTION)
                Log.debug("LWCAction: ignoring " + getActionName() + " on " + c);

        }

        void act(LWLink c) {
            ignoredDebug(c);
        }

        void act(LWNode c) {
            ignoredDebug(c);
        }

        void act(LWImage c) {
            ignoredDebug(c);
        }

        void act(LWSlide c) {
            ignoredDebug(c);
        }

        private void ignoredDebug(LWComponent c) {
            if (DEBUG.Enabled)
                Log.debug("LWCAction: ignoring " + getActionName() + " on " + c);
            //if (DEBUG.SELECTION) System.out.println("LWCAction: ignoring " + getActionName() + " on " + c);
        }

        void actOn(LWComponent c) {
            act(c);
        } // for manual init calls from internal code

        @Override
        public String getUndoName(ActionEvent e, Throwable exception) {
            String name = super.getUndoName(e, exception);
            if (selection().size() == 1)
                name += " (" + selection().first().getComponentTypeLabel() + ")";
            return name;
        }

        //public String toString() { return "LWCAction[" + getActionName() + "]"; }
    }

    public static final Action ResourcesAction = new ResourcesActionClass(
            MENU_INDENT + VueResources.local("dockWindow.contentPanel.resources.title"));

    public static class ResourcesActionClass extends VueAction {
        public ResourcesActionClass(String s) {
            super(s);
        }

        public void act() {
            VUE.getContentDock().setVisible(true);
            VUE.getContentPanel().showResourcesTab();
        }
    };

    public static final Action DatasetsAction = new DatasetsActionClass(
            MENU_INDENT + VueResources.local("dockWindow.contentPanel.datasets.title"));

    public static class DatasetsActionClass extends VueAction {
        public DatasetsActionClass(String s) {
            super(s);
        }

        public void act() {
            VUE.getContentDock().setVisible(true);
            VUE.getContentPanel().showDatasetsTab();
        }
    };

    public static final Action OntologiesAction = new OntologiesActionClass(
            MENU_INDENT + VueResources.local("dockWindow.contentPanel.ontologies.title"));

    public static class OntologiesActionClass extends VueAction {
        public OntologiesActionClass(String s) {
            super(s);
        }

        public void act() {
            VUE.getContentDock().setVisible(true);
            VUE.getContentPanel().showOntologiesTab();
        }
    };
}