/* * 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(); } }; }