/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * TableTop.java * Creation date: (10/18/00 12:33:25 PM) * By: Luke Evans */ package org.openquark.gems.client; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.KeyboardFocusManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.SystemColor; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Level; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.undo.StateEdit; import javax.swing.undo.UndoableEdit; import org.openquark.cal.compiler.FieldName; import org.openquark.cal.compiler.ModuleTypeInfo; import org.openquark.cal.compiler.QualifiedName; import org.openquark.cal.compiler.TypeException; import org.openquark.cal.compiler.TypeChecker.TypeCheckInfo; import org.openquark.cal.metadata.FunctionMetadata; import org.openquark.cal.services.GemEntity; import org.openquark.cal.services.Perspective; import org.openquark.cal.services.Status; import org.openquark.cal.valuenode.ValueNode; import org.openquark.gems.client.DisplayedGem.DisplayedPart; import org.openquark.gems.client.DisplayedGem.DisplayedPartConnectable; import org.openquark.gems.client.DisplayedGem.DisplayedPartInput; import org.openquark.gems.client.DisplayedGem.DisplayedPartOutput; import org.openquark.gems.client.Gem.PartConnectable; import org.openquark.gems.client.Gem.PartInput; import org.openquark.gems.client.Gem.PartOutput; import org.openquark.gems.client.GemGraph.InputCollectMode; import org.openquark.gems.client.GemGraph.TraversalScope; import org.openquark.gems.client.Graph.LayoutArranger.Tree; import org.openquark.gems.client.utilities.ExtendedUndoableEditSupport; import org.openquark.gems.client.valueentry.ValueEditor; import org.openquark.gems.client.valueentry.ValueEntryPanel; import org.openquark.util.Pair; import org.openquark.util.UnsafeCast; import org.openquark.util.html.HtmlHelper; import org.openquark.util.html.HtmlTransferable; import org.openquark.util.ui.ImageTransferable; import org.openquark.util.xml.BadXMLDocumentException; import org.openquark.util.xml.XMLPersistenceHelper; import org.w3c.dom.Document; import org.w3c.dom.DocumentFragment; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * The TableTop where Gems are combined etc. * This class represents the TableTop model, where a displayed gem hierarchy is added on top of the Gem Graph * hierarchy. The display component itself is stored in the TableTopPanel * Creation date: (10/18/00 12:33:25 PM) * @author Luke Evans */ /** * @author Neil Corkum * */ public class TableTop { /** The gap between 2 gems, set arbitrarily */ static final int GAP_BETWEEN_GEMS = 15; /** The added padding to bounds of a selection before taking a snapshot of the tabletop */ private static final int SNAPSHOT_BOUNDARY_PAD = 5; private final TableTopBurnManager burnManager; /** The JComponent that represents the tabletop */ private TableTopPanel tableTopPanel; // // Sundry properties // private final GemCutter gemCutter; private final GemGraph gemGraph; private Component focusStore; private final ExtendedUndoableEditSupport undoableEditSupport; private final CollectorArgumentStateEditable collectorArgumentStateEditable; /** The tabletop's display context for displayed gems. */ private final DisplayContext displayContext; /** The Shape provider for the tabletop. */ private final DisplayedGemShape.ShapeProvider shapeProvider = new TableTopShapeProvider(); /** The tabletop's listener for events on gems */ private final GemEventListener gemEventListener; /** The handler for code gem edits. */ private final CodeGemEditor.EditHandler codeGemEditHandler; /** The tabletop's listener on displayed gem state changes. */ private DisplayedGemStateListener gemStateListener; /** Listener for connection state. */ private DisplayedConnectionStateListener connectionStateListener; /** Map from gem to displayed gem. */ private final Map<Gem, DisplayedGem> gemDisplayMap; /** Map from connection to displayed connection. */ private final Map<Connection, DisplayedConnection> connectionDisplayMap; /** Map from input to cached value for that input, the last time the input had values entered for it from Run Mode. */ private final Map<PartInput, ValueNode> cachedArgValueMap; /** The gems in the TableTop which are selected. */ private final Set<DisplayedGem> selectedDisplayedGems = new HashSet<DisplayedGem>(); /** The gems in the TableTop which are running. */ private final Set<DisplayedGem> runningDisplayedGems = new HashSet<DisplayedGem>(); /** The connections in the TableTop which are considered "bad". */ private final Set<DisplayedConnection> badDisplayedConnections = new HashSet<DisplayedConnection>(); /** Maps codegems to their editors. All DisplayedCodeGems should have an editor in the map. */ private final Map<CodeGem, CodeGemEditor> codeGemEditorMap; /** The code gem editor map for unplaced code gem editors. */ private final Map<CodeGem, CodeGemEditor> unplacedCodeGemEditorMap; /** The DisplayedGem that currently has focus. */ private DisplayedGem focusedDisplayedGem; /** The target collector for this tabletop. */ private final DisplayedGem targetDisplayedCollector; // final, unless loaded. TODOEL: remove (calculate from gem graph's target) /** * As we expand and shrink the tabletop, the position of the 'original' origin is changed. * In order to maintain positional validity in historical data such as undo's, we'll store all such * relative to this historical origin. */ private Point originalOrigin = new Point(0,0); /** * Encapsulates information that displayed gems need to determine how to display themselves. * We don't want displayed gems to hold a reference to the table top directly. * @author Frank Worsley */ public class DisplayContext { ModuleTypeInfo getContextModuleTypeInfo() { return getCurrentModuleTypeInfo(); } } /** * The Shape Provider for the TableTop. * @author Edward Lam */ private class TableTopShapeProvider implements DisplayedGemShape.ShapeProvider { /** * {@inheritDoc} */ public DisplayedGemShape getDisplayedGemShape(DisplayedGem displayedGem) { Gem gem = displayedGem.getGem(); if (gem instanceof CodeGem || gem instanceof FunctionalAgentGem) { return new DisplayedGemShape.Triangular(displayedGem); } else if (gem instanceof CollectorGem) { return new DisplayedGemShape.Oval(displayedGem); } else if (gem instanceof RecordFieldSelectionGem) { return new DisplayedGemShape.Triangular(displayedGem); } else if (gem instanceof RecordCreationGem) { return new DisplayedGemShape.Triangular(displayedGem); } else if (gem instanceof ReflectorGem) { boolean ovalShape = (gem.getNInputs() == 0); if (ovalShape) { return new DisplayedGemShape.Oval(displayedGem); } else { return new DisplayedGemShape.Triangular(displayedGem); } } else if (gem instanceof ValueGem) { final ValueGem valueGem = (ValueGem)gem; // Rectangular.. DisplayedGemShape.InnerComponentInfo info = new DisplayedGemShape.InnerComponentInfo() { public Rectangle getBounds() { // Check for the case where the panel is not created yet.. ValueEntryPanel valueEntryPanel = getTableTopPanel().getValueEntryPanel(valueGem); if (valueEntryPanel == null) { return new Rectangle(); } return valueEntryPanel.getBounds(); } public void paint(TableTop tableTop, Graphics2D g2d) { // Paint a fake VEP image so that the VEP can appear below other gems that might obscure this one. ValueEntryPanel valueEntryPanel = tableTop.getTableTopPanel().getValueEntryPanel(valueGem); Rectangle bounds = valueEntryPanel.getBounds(); Graphics vepGraphics = g2d.create(bounds.x, bounds.y, bounds.width, bounds.height); // For some reason you can't just say vep.paint(vepGraphics).. valueEntryPanel.paintOnTableTop(vepGraphics); vepGraphics.dispose(); } public List<Rectangle> getInputNameLabelBounds() { return null; } }; return new DisplayedGemShape.Rectangular(displayedGem, info); } else { throw new IllegalArgumentException("Unrecognized gem type: " + gem.getClass()); } } } /** * The TableTop's handler for code gem edits. * @author Edward Lam */ private class TableTopCodeGemEditHandler implements CodeGemEditor.EditHandler { /** * {@inheritDoc} */ public void definitionEdited(CodeGemEditor codeGemEditor, PartInput[] oldInputs, UndoableEdit codeGemEdit) { CodeGem codeGem = codeGemEditor.getCodeGem(); // Get the pre-state of argument target-related changes. // Note that we have to keep track of targeting info for arguments which disappear // (eg. a code gem input disappearing may cause emitter inputs to disappear as well!). // Get the emitter and collector argument state from before the disconnection. StateEdit collectorArgumentStateEdit = new StateEdit(collectorArgumentStateEditable); // The code gem has already updated, but arguments have not. getGemGraph().retargetArgumentsForDefinitionChange(codeGem, oldInputs); // Update the code gem editor state for connectivity. updateForConnectivity(codeGemEditor); // Post the edit to the undo manager. undoableEditSupport.postEdit(new UndoableCodeGemDefinitionEdit(getGemGraph(), codeGemEdit, collectorArgumentStateEdit)); // Update the tabletop for the new gem graph state. updateForGemGraph(); } } /** * A listener for events on gems and display gems. * Creation date: (02/25/02 6:16:00 PM) * @author Edward Lam */ private class GemEventListener implements BurnListener, InputChangeListener, GemStateListener, NameChangeListener, TypeChangeListener, CodeGemDefinitionChangeListener, DisplayedGemLocationListener, DisplayedGemSizeListener, DisplayedGemStateListener, DisplayedConnectionStateListener { /** * Notify listeners that the validity of a connection changed. * @param e ConnectionStateEvent the related event. */ public void badStateChanged(DisplayedConnectionStateEvent e) { // just repaint the bounds DisplayedConnection dConn = (DisplayedConnection)e.getSource(); getTableTopPanel().repaint(dConn.getBounds()); } /** * Notify listeners that an input was burned. * @param e BurnEvent the related event. */ public void burntStateChanged(BurnEvent e){ // Repaint the input which burnt. getTableTopPanel().repaint(getDisplayedPartConnectable((PartInput)e.getSource()).getBounds()); } /** * Notify listeners that the gem size changed. * @param e DisplayedGemSizeEvent the related event. */ public void gemSizeChanged(DisplayedGemSizeEvent e){ DisplayedGem dGemSource = (DisplayedGem)e.getSource(); Rectangle oldBounds = e.getOldBounds(); // repaint old and new selection bounds getTableTopPanel().repaint(getSelectedBounds(dGemSource, oldBounds)); getTableTopPanel().repaint(getSelectedBounds(dGemSource)); // update connections updateDisplayedConnections(dGemSource); } /** * Notify listeners that the gem location changed. * @param e DisplayedGemLocationEvent the related event. */ public void gemLocationChanged(DisplayedGemLocationEvent e) { DisplayedGem dGemSource = (DisplayedGem)e.getSource(); Rectangle oldBounds = e.getOldBounds(); // the location didn't actually change if (oldBounds.getLocation().equals(dGemSource.getLocation())) { return; } // repaint old and new selection bounds getTableTopPanel().repaint(getSelectedBounds(dGemSource, oldBounds)); getTableTopPanel().repaint(getSelectedBounds(dGemSource)); // update connections updateDisplayedConnections(dGemSource); // Notify the panel if it was a value gem. // TODOEL: Move this method/listener (and others) to TableTopPanel. This should not be delegated. if (dGemSource.getGem() instanceof ValueGem) { tableTopPanel.handleValueGemMoved((ValueGem)dGemSource.getGem()); } } /** * {@inheritDoc} */ public void nameChanged(NameChangeEvent e) { Gem sourceGem = (Gem)e.getSource(); getDisplayedGem(sourceGem).sizeChanged(); } /** * Notify listeners that the run state changed. * @param e DisplayedGemStateEvent the related event. */ public void runStateChanged(DisplayedGemStateEvent e) { DisplayedGem dGemSource = (DisplayedGem)e.getSource(); // repaint gem selection bounds getTableTopPanel().repaint(getSelectedBounds(dGemSource)); } /** * Notify listeners that the selection state changed. * @param e DisplayedGemStateEvent the related event. */ public void selectionStateChanged(DisplayedGemStateEvent e){ // repaint gem selection bounds DisplayedGem dGemSource = (DisplayedGem)e.getSource(); getTableTopPanel().repaint(getSelectedBounds(dGemSource)); } /** * Notify listeners that the broken state changed. * @param e GemStateEvent the related event. */ public void brokenStateChanged(GemStateEvent e){ // Don't have to do anything. Repaint events are triggered by definition change. } /** * Notify listeners that a code gem definition has changed * @param e GemDefinitionEvent the related event. */ public void codeGemDefinitionChanged(CodeGemDefinitionChangeEvent e) { // inputsChanged() doesn't get called if the code gem unbreaks, but has 0 inputs. getTableTopPanel().repaint(getDisplayedGem((CodeGem)e.getSource()).getBounds()); } /** * {@inheritDoc} */ public void inputsChanged(InputChangeEvent e) { Gem sourceGem = (Gem)e.getSource(); DisplayedGem displayedGem = getDisplayedGem(sourceGem); // Figure out the old and new sizes. int oldArity = displayedGem.getNDisplayedArguments(); int newArity = displayedGem.getGem().getNInputs(); // Update the displayed inputs. displayedGem.updateDisplayedInputs(); getTableTopPanel().repaint(displayedGem.getBounds()); // Emit a size change event, if applicable. // TODOEL: any size change event will cause this listener to repaint the bounds too. if (oldArity != newArity) { displayedGem.sizeChanged(); } // connections may have moved around updateDisplayedConnections(displayedGem); } /** * {@inheritDoc} */ public void typeChanged(TypeChangeEvent e) { // When a part's type changes, repaint for the new type. PartConnectable partChanged = (PartConnectable)e.getSource(); DisplayedPartConnectable displayedPartConnectable = getDisplayedPartConnectable(partChanged); if (displayedPartConnectable != null) { getTableTopPanel().repaint(displayedPartConnectable.getBounds()); } } } /** * A comparator that will sort DisplayedGems alphabetically. Comparison will be based on * the value of a DisplayedValueGem or the name of all the other varieties of DisplayedGems. */ private static final class DisplayedGemComparator implements Comparator<DisplayedGem> { public int compare(DisplayedGem o1, DisplayedGem o2) { Gem gem1 = o1.getGem(); Gem gem2 = o2.getGem(); // Get the comparison text for the first argument String text1; if (gem1 instanceof ValueGem) { text1 = ((ValueGem)gem1).getStringValue(); } else if (gem1 instanceof NamedGem) { text1 = ((NamedGem)gem1).getUnqualifiedName(); } else { throw new IllegalArgumentException("Unknown DisplayedGem " + o1); } // Get the comparison text for the second argument String text2; if (gem2 instanceof ValueGem) { text2 = ((ValueGem)gem2).getStringValue(); } else if (gem2 instanceof NamedGem) { text2 = ((NamedGem)gem2).getUnqualifiedName(); } else { throw new IllegalArgumentException("Unknown DisplayedGem " + o2); } return text1.compareTo(text2); } } /** * TableTop default constructor. * @param gemCutter GemCutter the gemCutter of which this table top is part. */ public TableTop(GemCutter gemCutter) { this.gemCutter = gemCutter; displayContext = new DisplayContext(); gemDisplayMap = new LinkedHashMap<Gem, DisplayedGem>(); connectionDisplayMap = new HashMap<Connection, DisplayedConnection>(); // Use a weak hash map to reclaim unused value nodes when partInputs no longer used cachedArgValueMap = new WeakHashMap<PartInput, ValueNode>(); // Use a weak collections so that we don't have to keep track of when the displayed code gem is no longer referenced. codeGemEditorMap = new WeakHashMap<CodeGem, CodeGemEditor>(); unplacedCodeGemEditorMap = new WeakHashMap<CodeGem, CodeGemEditor>(); // Instantiate some new members gemGraph = new GemGraph(); collectorArgumentStateEditable = new CollectorArgumentStateEditable(gemGraph); targetDisplayedCollector = createDisplayedGem(gemGraph.getTargetCollector(), new Point()); gemEventListener = new GemEventListener(); codeGemEditHandler = new TableTopCodeGemEditHandler(); undoableEditSupport = new ExtendedUndoableEditSupport(this); burnManager = new TableTopBurnManager(this); // Add the gem event listener as a state change listener. addStateChangeListener(gemEventListener); // Set the TableTop's focus traversal policy here. This policy is needed due to the fact that // when a component (such as a Value Editor) is removed from a container (such as the TableTop) // and it has focus, the focus will be shifted to the next component in the container's focus cycle. // This extra focus event interferes with the desired operation of the Value Editors so we want to // prevent it. The focus event can be prevented by pretending there isn't a next component in the // focus cycle. The Java code will first try to find the next component for the Value Editor and // if it is null, it will try to find the focus cycle's default component. If we say the default // component is null then a focus event is not created. } /** * Set up the target such that it is in an appropriate state for a new tabletop. */ void resetTargetForNewTableTop() { // TODOEL: make private. String baseName = "result"; String targetName = baseName; int i = 1; while (gemCutter.getPerspective().getVisibleGemEntity(QualifiedName.make(gemCutter.getWorkingModuleName(), targetName)) != null) { targetName = baseName + i; i++; } getTargetCollector().setName(targetName); // Don't notify the undo manager since this process should not be undoable. // Place it at the top right corner, leaving some space around it. // TODOEL: If the user invokes the "new" action several times quickly in sequence, the tabletop may not have had time to // re-institute itself. The visible rect width would be zero, resulting in the gem being placed in the top left corner. int x = getTableTopPanel().getVisibleRect().width - targetDisplayedCollector.getBounds().width - 2 * DisplayConstants.HALO_SIZE; int y = 2 * DisplayConstants.HALO_SIZE; Point gemLocation = new Point(x, y); if (!gemDisplayMap.containsKey(targetDisplayedCollector.getGem())) { // This is the case when the tabletop is first created. // Ensure that the target collector is known to the tabletop data structures.. addGemAndUpdate(targetDisplayedCollector, gemLocation); // Also make sure it gets painted. getTableTopPanel().repaint(getSelectedBounds(targetDisplayedCollector)); } else { // The tabletop exists already, but a "new" action was invoked. // Just reset the target location. doChangeGemLocationUserAction(targetDisplayedCollector, gemLocation); } } /** * Update the state of the TableTop for changes in the GemGraph model. * The gem graph will be retyped, arg names disambiguated, and value gem panels revalidated. */ void updateForGemGraph() { // update types try { gemGraph.typeGemGraph(getTypeCheckInfo()); } catch (TypeException te) { GemCutter.CLIENT_LOGGER.log(Level.SEVERE, "Error type checking tabletop."); } // Update the panel for new editability state. getTableTopPanel().revalidateValueGemPanels(); } /** * Get the displayed gem corresponding to a model gem * @param gem Gem the (model) gem in question * @return DisplayedGem the corresponding displayed gem */ final DisplayedGem getDisplayedGem(Gem gem){ return gemDisplayMap.get(gem); } /** * Get all the DisplayedGems on the TableTop. */ Set<DisplayedGem> getDisplayedGems() { Set<DisplayedGem> displayedGems = new LinkedHashSet<DisplayedGem>(); for (final Gem gem : gemDisplayMap.keySet()) { displayedGems.add(gemDisplayMap.get(gem)); } return displayedGems; } /** * Get all the DisplayedConnections on the TableTop. */ Set<DisplayedConnection> getDisplayedConnections() { return new HashSet<DisplayedConnection>(connectionDisplayMap.values()); } /** * Create a displayed gem at the given location. * @param gem the gem to wrap in a displayed gem. * @param location the location of the displayed gem. * @return the resulting displayed gem, suitable for display on the tabletop. */ DisplayedGem createDisplayedGem(Gem gem, Point location) { return new DisplayedGem(displayContext, shapeProvider, gem, location); } /** * Create a displayed gem wrapping a new code gem at the given location. * @param location the location of the displayed gem. * @return the resulting displayed gem, suitable for display on the tabletop. */ DisplayedGem createDisplayedCodeGem(Point location) { return createDisplayedGem(new CodeGem(), location); } /** * Create a displayed gem wrapping a new collector gem at the given location. * @param location the location of the displayed gem. * @param targetCollector the new collector's target collector. * @return the resulting displayed gem, suitable for display on the tabletop. */ DisplayedGem createDisplayedCollectorGem(Point location, CollectorGem targetCollector) { return createDisplayedGem(new CollectorGem(targetCollector), location); } /** * Create a displayed gem wrapping a new RecordFieldSelection gem at the given location. * @param location the location of the displayed gem. * @return the resulting displayed gem, suitable for display on the tabletop. */ DisplayedGem createDisplayedRecordFieldSelectionGem(Point location) { return createDisplayedGem(new RecordFieldSelectionGem(), location); } /** * Create a displayed gem wrapping a new RecordCreation gem at the given location. * @param location the location of the displayed gem. * @return the resulting displayed gem, suitable for display on the tabletop. */ DisplayedGem createDisplayedRecordCreationGem(Point location) { return createDisplayedGem(new RecordCreationGem(),location); } /** * Create a displayed gem wrapping a new functional agent gem at the given location. * @param location the location of the displayed gem. * @param gemEntity the entity from which to construct the gem. * @return the resulting displayed gem, suitable for display on the tabletop. */ DisplayedGem createDisplayedFunctionalAgentGem(Point location, GemEntity gemEntity) { return createDisplayedGem(new FunctionalAgentGem(gemEntity), location); } /** * Create a displayed gem wrapping a new reflector gem at the given location. * @param location the location of the displayed gem. * @param collectorGem the emitter gem's collector. * @return the resulting displayed gem, suitable for display on the tabletop. */ DisplayedGem createDisplayedReflectorGem(Point location, CollectorGem collectorGem) { return createDisplayedGem(new ReflectorGem(collectorGem), location); } /** * Create a displayed gem wrapping a new value gem at the given location. * @param location the location of the displayed gem. * @return the resulting displayed gem, suitable for display on the tabletop. */ DisplayedGem createDisplayedValueGem(Point location) { return createDisplayedGem(new ValueGem(), location); } /** * Get the target for this tabletop. * @return the target gem for this tabletop. */ public CollectorGem getTargetCollector() { return (CollectorGem)targetDisplayedCollector.getGem(); } /** * Get the target for this tabletop. * @return the target gem for this tabletop. */ public DisplayedGem getTargetDisplayedCollector() { return targetDisplayedCollector; } /** * Get the corresponding display part for a given connectable part * @param part the part in question * @return the corresponding displayed part, or none if it does not exist. */ DisplayedPartConnectable getDisplayedPartConnectable(PartConnectable part) { DisplayedGem displayedGem = getDisplayedGem(part.getGem()); if (part instanceof PartInput) { // Check that the input number is within bounds. int inputIndex = ((PartInput)part).getInputNum(); if (inputIndex < displayedGem.getNDisplayedArguments()) { // Check that the displayed input corresponds to the input part. DisplayedPartInput displayedPartInput = displayedGem.getDisplayedInputPart(inputIndex); if (displayedPartInput.getPartInput() == part) { return displayedPartInput; } } // We might be in an inconsistent state. Check against all displayed input parts.. for (int i = 0, nDisplayedArgs = displayedGem.getNDisplayedArguments(); i < nDisplayedArgs; i++) { DisplayedPartInput displayedPartInput = displayedGem.getDisplayedInputPart(i); if (displayedPartInput.getPartInput() == part) { return displayedPartInput; } } return null; } else if (part instanceof PartOutput) { return displayedGem.getDisplayedOutputPart(); } else { throw new Error("Can't get the displayed part for this part: " + part.getClass()); } } /** * Return the part of a gem under this point. * @param xy Point the coordinate to look under * @return DisplayedPart the part of the Gem under coord xy, or null if none are hit */ DisplayedPart getGemPartUnder(Point xy) { // Hit test for each Gem. DisplayedPart hitPart = null; for (final Gem gem : gemDisplayMap.keySet()) { DisplayedGem thisGem = gemDisplayMap.get(gem); // Ask the gem what part of it has been hit DisplayedPart thisGemHitPart = thisGem.whatHit(xy); if (thisGemHitPart != null) { hitPart = thisGemHitPart; } } return hitPart; } /** * Return the gem under this point. * @param xy Point the coordinate to look under * @return DisplayedGem the Gem under coord xy, or null if none are hit */ DisplayedGem getGemUnder(Point xy) { // Hit test for each Gem. DisplayedGem gemHit = null; for (final Gem gem : gemDisplayMap.keySet()){ // Get the next element for consideration DisplayedGem thisGem = gemDisplayMap.get(gem); // Now ask it if it has been hit (overall) if (thisGem.anyHit(xy)) { gemHit = thisGem; } } return gemHit; } /** * Get the list of currently selected Gems. * @return Gem[] an array of all the selected gems in the tabletop */ public Gem[] getSelectedGems() { // Start a list of gems List<Gem> selected = new ArrayList<Gem>(); // Consider each Gem for (final Gem gem : gemGraph.getGems()){ // Get the next element for consideration DisplayedGem displayedGem = getDisplayedGem(gem); // If this is selected, add it to the list if (displayedGem != null && isSelected(displayedGem)) { selected.add(gem); } } // Make return array and return it Gem[] gemArray = new Gem[selected.size()]; gemArray = selected.toArray(gemArray); return gemArray; } /** * Get the list of currently selected Gems. * @return DisplayedGem[] an array of all the selected display gems in the tabletop */ public DisplayedGem[] getSelectedDisplayedGems() { // Start a list of gems List<DisplayedGem> selected = new ArrayList<DisplayedGem>(); // Consider each Gem for (final DisplayedGem displayedGem : getDisplayedGems()){ // If this is selected, add it to the list if (isSelected(displayedGem)) { selected.add(displayedGem); } } // Make return array and return it DisplayedGem[] gemArray = new DisplayedGem[selected.size()]; gemArray = selected.toArray(gemArray); return gemArray; } /** * Get the DisplayedGem that currently has focus. * @return DisplayedGem */ DisplayedGem getFocusedDisplayedGem() { return focusedDisplayedGem; } /** * Give focus to a new DisplayedGem. * @param focusedDisplayedGem */ void setFocusedDisplayedGem(DisplayedGem focusedDisplayedGem) { // Make the new gem the one with focus, but remember the old gem. DisplayedGem oldFocusedDisplayedGem = this.focusedDisplayedGem; this.focusedDisplayedGem = focusedDisplayedGem; if (oldFocusedDisplayedGem != null) { if (oldFocusedDisplayedGem.getGem() instanceof ValueGem) { tableTopPanel.getValueEntryPanel((ValueGem)oldFocusedDisplayedGem.getGem()).setVisible(false); } getTableTopPanel().repaint(oldFocusedDisplayedGem.getBounds()); } if (focusedDisplayedGem != null) { if (focusedDisplayedGem.getGem() instanceof ValueGem) { tableTopPanel.getValueEntryPanel((ValueGem)focusedDisplayedGem.getGem()).setVisible(true); } // Reinsert new gem into the map so that it appears as the gem drawn topmost gemDisplayMap.remove(focusedDisplayedGem.getGem()); gemDisplayMap.put(focusedDisplayedGem.getGem(), focusedDisplayedGem); // Now repaint the new gem getTableTopPanel().repaint(focusedDisplayedGem.getBounds()); } } /** * Returns the DisplayedGem that is closest to the supplied gem in the specified direction. * Returns null if there is no DisplayedGem in the specified direction. * @param direction NavigationDirection - the direction to search * @param currentGem DisplayedGem - the DisplayedGem to use as the origin of the search * @return DisplayedGem */ DisplayedGem findNearestDisplayedGem(TableTopPanel.NavigationDirection direction, DisplayedGem currentGem) { // If the current gem is null then simply return here. if (currentGem == null) { return null; } // First figure out which direction we are to search. double lowerBearingRange, upperBearingRange; if (direction == TableTopPanel.NavigationDirection.UP) { upperBearingRange = 45; lowerBearingRange = 315; } else if (direction == TableTopPanel.NavigationDirection.RIGHT) { lowerBearingRange = 45; upperBearingRange = 135; } else if (direction == TableTopPanel.NavigationDirection.DOWN) { lowerBearingRange = 135; upperBearingRange = 225; } else if (direction == TableTopPanel.NavigationDirection.LEFT) { lowerBearingRange = 225; upperBearingRange = 315; } else { // Somehow we got a weird direction as an argument throw new IllegalArgumentException("Unknown direction: " + direction); } DisplayedGem nextGem = null; double distToNextGem = -1; List<DisplayedGem> stackedGems = new ArrayList<DisplayedGem>(); // To track the Gems that have the same center point // Iterate over all the displayed gems. for (final DisplayedGem dGem : getDisplayedGems()) { // Add the current gem to the stacked gems collection so it is in the proper order if a tie breaker is // needed. if (dGem == currentGem) { stackedGems.add(dGem); continue; } double bearing = calculateBearingBetweenDisplayedGems(currentGem, dGem); // If the bearing is -1 then the two Gems are right on top of each other so remember this. if (bearing == -1) { stackedGems.add(dGem); continue; } if ((direction == TableTopPanel.NavigationDirection.UP && (bearing > lowerBearingRange || bearing <= upperBearingRange)) || (bearing > lowerBearingRange && bearing <= upperBearingRange)) { // We have found a gem that is in the right direction. // See if it is nearer than the last one we have been holding onto double dist = currentGem.getCenterPoint().distance(dGem.getCenterPoint()); if (distToNextGem == -1 || dist < distToNextGem) { nextGem = dGem; distToNextGem = dist; } } } // If there are two or more gems in the stacked gems list (there should always be at least 1) we have // have a tie-breaker to figure out which one to go to next if (stackedGems.size() > 1) { // Try to get the next Gem based on alphabetical order. If we get one back then use it. DisplayedGem possibleNextGem = findNextGemAlphabetically(currentGem, stackedGems, direction); if (possibleNextGem != null) { nextGem = possibleNextGem; } } // Return the next gem (it will be null if there are no Gems in the desired direction). return nextGem; } /** * Return the next DisplayedGem in the supplied collection based on alphabetical order and direction. * Directions of UP and LEFT will return the gem alphabetically before the current gem while * DOWN and RIGHT will return the gem alphabetically after the current gem in the supplied collection. * Null will be returned if there isn't a next gem. DisplayedValueGems will be alphabetized according to * its actual value while the other DisplayedGems will be sorted based on their names. * NOTE: the current gem must be included in the collection of DisplayedGems. * * @param currentGem find the DisplayedGem that follows this one alphabetically * @param gems the collection of Gems to look in * @param direction determines which way to go in alphabetical order to get the next Gem * @return DisplayedGem */ private static DisplayedGem findNextGemAlphabetically(DisplayedGem currentGem, Collection<DisplayedGem> gems, TableTopPanel.NavigationDirection direction) { // Make sure the current gem is in the collection of displayed gems if (!gems.contains(currentGem)) { throw new Error("Programming Error: currentGem must be in the DisplayedGem collection"); } // Copy the collection so we can play with it List<DisplayedGem> gemList = new ArrayList<DisplayedGem>(gems); // Sort the copied collection Collections.sort(gemList, new DisplayedGemComparator()); // Calculate the index of the displayed gem to return based on the index of the current gem. int nextIndex = -1; if (direction == TableTopPanel.NavigationDirection.UP || direction == TableTopPanel.NavigationDirection.LEFT) { nextIndex = gemList.indexOf(currentGem) - 1; } else if (direction == TableTopPanel.NavigationDirection.DOWN || direction == TableTopPanel.NavigationDirection.RIGHT) { nextIndex = gemList.indexOf(currentGem) + 1; } // Return the correct displayed gem. if (nextIndex >= 0 && nextIndex < gemList.size()) { return gemList.get(nextIndex); } else { // No Gem to return return null; } } /** * Returns the bearing between the center points of two DisplayedGems. * The bearing is greater than or equal to 0, but less than 360 degrees. * 0 degrees is at the 12 o'clock position, 90 degrees is at 3 o'clock, 180 degrees is at 6 o'clock * and 270 degrees is at 9 o'clock. If the center points of the two Gems are right on top of * each other then -1 is returned. * * @param fromDGem DisplayedGem - the starting DisplayedGem * @param toDGem DisplayedGem - the ending DisplayedGem * @return double - the bearing between the fromDGem and toDGem in degrees */ private static double calculateBearingBetweenDisplayedGems(DisplayedGem fromDGem, DisplayedGem toDGem) { // Get the locations of the two DisplayedGems Point2D fromCP = fromDGem.getCenterPoint(); Point2D toCP = toDGem.getCenterPoint(); // Calculate the deltas (remember the co-ordinate system begins in the top left corner of the TableTop // with increasing X's going to the right and increasing Y's going down). double deltaX = toCP.getX() - fromCP.getX(); double deltaY = fromCP.getY() - toCP.getY(); // Check that the two center points are not the same. if (deltaX == 0 && deltaY == 0) { return -1; } // Find the angle and adjust for which quadrant we're in double angle = Math.toDegrees(Math.atan(deltaX / deltaY)); if (toCP.getY() > fromCP.getY()){ // We're in the lower hemisphere - angles in the lower right quadrant are -90 to 0 and // angles in the lower left quadrant are 0 to 90 (all rotating clockwise) // so adding 180 gives us the correct bearing return 180 + angle; } else { if (toCP.getX() < fromCP.getX()) { // We're in the upper left quadrant - angles run from 0 to -90 (going counter clockwise) return 360 + angle; } else { // We're in the upper right quadrant - the angle is the correct bearing return angle; } } } /** * Determines where a DisplayedGem should be located. * @param displayedGem DisplayedGem - the DisplayedGem that needs a home. * @return Point */ Point findAvailableDisplayedGemLocation(DisplayedGem displayedGem) { // Get the existing viewable TableTop area and the DisplayedGem dimensions Rectangle dGemBounds = displayedGem.getBounds(); Rectangle visibleRect = getTableTopPanel().getVisibleRect(); int visibleRectMaxX = (int)visibleRect.getMaxX(); int visibleRectMaxY = (int)visibleRect.getMaxY(); // Set up a variable to track where (in the Y direction) to start the next search row int initialFreeRowValue = 100000; int nextFreeRow = initialFreeRowValue; // Determine a rectangle where we can begin searching for a free spot Rectangle searchRect = new Rectangle(visibleRect.x, visibleRect.y, dGemBounds.width, dGemBounds.height); // Keep looping until we find a free spot or until the algorithm gives up. while (true) { // Does the search rectangle intersect any existing Gem bounds? Rectangle collisionRect = findDisplayedGemPositionCollisions(searchRect); if (collisionRect == null) { // No collisions were found... we found a free space return searchRect.getLocation(); } else { // There were collisions so move our search rectangle just past the collision area searchRect.x = (int)collisionRect.getMaxX() + TableTopPanel.SEARCH_HORIZONTAL_SPACING; // Remember the lowest Y value for the collision bounds so we know a safe place to start // the next search row nextFreeRow = (int)Math.min(nextFreeRow, collisionRect.getMaxY()); } // We didn't find a free space and have moved the search rectangle. Make sure the new rectangle // will allow enough room for the DisplayedGem to fit into the visible area horizontally if (visibleRectMaxX < searchRect.getMaxX()) { // The DisplayedGem won't fit horizontally so try to search one row down if (visibleRectMaxY < (nextFreeRow + searchRect.height)) { // If we continue searching 1 more row down, the DisplayedGem won't fit into the viewable area // vertically so just place the DisplayedGem at the origin of the visible area return visibleRect.getLocation(); } else { // We can fit at least one more search row in and we are currently off the right side // of the visible area so start searching again from the left side but 1 row down searchRect.x = visibleRect.x; searchRect.y = nextFreeRow; // Reset the variable that tracks the next free row nextFreeRow = initialFreeRowValue; } } } } /** * Helper function that tests a potential DisplayedGem position for collisions with * existing DisplayedGems. Returns the spanning bounds of all the DisplayedGems with which * collisions occurred or null if there were no collisions. * @param possibleRect Rectangle - the area to check for existing DisplayedGems. * @return Rectangle */ private Rectangle findDisplayedGemPositionCollisions(Rectangle possibleRect) { // Use a rectangle to track the bounds of any DisplayedGems that we collide with Rectangle collisionRect = null; // Iterate over the existing DisplayedGems and see if the possible position conflicts with anything. for (final Gem gem : gemDisplayMap.keySet()){ Rectangle dGemBounds = gemDisplayMap.get(gem).getBounds(); if (possibleRect.intersects(dGemBounds)) { if (collisionRect == null) { collisionRect = dGemBounds; } else { collisionRect.add(dGemBounds); } } } return collisionRect; } /** * Get the code gem editor associated with a code gem. * @param codeGem the code gem whose code editor to return. This may be null if a code editor * has not yet been created (ie. never shown). * @return CodeGemEditor the code editor associated with this displayed code gem */ CodeGemEditor getCodeGemEditor(CodeGem codeGem) { return codeGemEditorMap.get(codeGem); } /** * Returns false if the proposed name already exists on the desktop as * a collector or a code gem, Returns true otherwise. * @param name * @param gem the gem to be renamed */ boolean isAvailableCodeOrCollectorName(String name, Gem gem) { // must check that the name is not the name of another collector Set<CollectorGem> matchingCollectors = gemGraph.getCollectorsForName(name); for (final CollectorGem matchingCollector : matchingCollectors) { if (matchingCollector != gem) { return false; } } // Find all the names on the tabletop and see if the proposed name is among them // and that the owner of the name is not the gem to be renamed. Set<Gem> allGems = gemGraph.getGems(); Set<String> otherCodeGems = new HashSet<String>(); for (final Gem nextGem : allGems) { if (nextGem instanceof CodeGem && nextGem != gem) { otherCodeGems.add(((CodeGem)nextGem).getUnqualifiedName()); } } return (!otherCodeGems.contains(name)); } /** * Returns whether or not a code gem's editor is visible. * @param codeGem the code gem whose code editor is in question. * @return boolean true if the code editor is visible (of if it's null). */ boolean isCodeEditorVisible(CodeGem codeGem) { CodeGemEditor codeGemEditor = getCodeGemEditor(codeGem); if (codeGemEditor == null) { return false; } return codeGemEditor.isVisible(); } /** * Get a new code gem editor for a given codegem. * @param codeGem the code gem whose code editor should be shown/hidden * @return a new CodeGemEditor for the code gem. */ private CodeGemEditor getNewCodeGemEditor(final CodeGem codeGem) { // Create the code editor CodeGemEditor codeGemEditor = new CodeGemEditor(gemCutter, codeGem, codeGemEditHandler, gemCutter.getPerspective(), gemGraph, gemCutter.getNavigatorOwner(), false, gemCutter.getCodeGemAnalyser()); // Set it up String title = GemCutterMessages.getString("CGE_EditorTitle", codeGem.getUnqualifiedName()); codeGemEditor.setName(title); codeGemEditor.setTitle(title); codeGemEditor.setResizable(true); // Alter the default behaviour of the editor so that it does nothing on closing and // handle the closing here codeGemEditor.setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE); codeGemEditor.addWindowListener(new java.awt.event.WindowAdapter() { @Override public void windowClosing(java.awt.event.WindowEvent evt) { showCodeGemEditor(codeGem, false); } }); // Add a listener to the code editor that will cause the associated code gem to be // repainted when the editor is shown or hidden (so that the open editor indicator is painted). codeGemEditor.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent evt) { CodeGemEditor codeEditor = (CodeGemEditor)evt.getComponent(); DisplayedGem displayedCodeGem = getDisplayedGem(codeEditor.getCodeGem()); getTableTopPanel().repaint(getSelectedBounds(displayedCodeGem)); } @Override public void componentHidden(ComponentEvent evt) { CodeGemEditor codeEditor = (CodeGemEditor)evt.getComponent(); DisplayedGem displayedCodeGem = getDisplayedGem(codeEditor.getCodeGem()); // May be null if the code gem is being deleted. if (displayedCodeGem != null) { getTableTopPanel().repaint(getSelectedBounds(displayedCodeGem)); } } }); codeGemEditor.getGlassPane().addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent evt) { gemCutter.enterGUIState(GemCutter.GUIState.EDIT); } }); // Add a listener to the code editor that will cause the associated code gem to be repainted // when the editor is activated or deactivated (so the proper open editor indicator is displayed); codeGemEditor.addWindowListener(new WindowAdapter() { @Override public void windowActivated(WindowEvent evt) { if (gemCutter.getGUIState() == GemCutter.GUIState.ADD_GEM) { gemCutter.enterGUIState(GemCutter.GUIState.EDIT); } CodeGemEditor codeEditor = (CodeGemEditor)evt.getWindow(); DisplayedGem codeGem = getDisplayedGem(codeEditor.getCodeGem()); getTableTopPanel().repaint(getSelectedBounds(codeGem)); } @Override public void windowDeactivated(WindowEvent evt) { CodeGemEditor codeEditor = (CodeGemEditor)evt.getWindow(); DisplayedGem codeGem = getDisplayedGem(codeEditor.getCodeGem()); getTableTopPanel().repaint(getSelectedBounds(codeGem)); } }); return codeGemEditor; } /** * Show or hide a CodeGem's codeEditor. * @param codeGem the code gem whose code editor should be shown/hidden * @param show boolean show it if true, otherwise hide if it is showing */ void showCodeGemEditor(CodeGem codeGem, boolean show) { if (show) { // If this is the first time we're displaying the editor, set its position to be right // below the code gem. CodeGemEditor codeGemEditor = unplacedCodeGemEditorMap.remove(codeGem); if (codeGemEditor != null) { codeGemEditor.doSyntaxSmarts(); Rectangle gemBounds = getDisplayedGem(codeGem).getBounds(); Point editorPt = new Point(gemBounds.x + 15, gemBounds.y + gemBounds.height); SwingUtilities.convertPointToScreen(editorPt, TableTop.this.getTableTopPanel()); codeGemEditor.setLocation(editorPt); } // Get the code gem editor, even if it's already placed. codeGemEditor = codeGemEditorMap.get(codeGem); // Make sure the editor is completely visible on the screen when it is opened. Make sure the bottom, // right, left, and top edges are visible in this order to ensure that the top left // corner will always be visible. Rectangle editorBounds = codeGemEditor.getBounds(); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); if (screenSize.height < editorBounds.getMaxY()) { editorBounds.y -= (editorBounds.getMaxY() - screenSize.height); } if (screenSize.width < editorBounds.getMaxX()) { editorBounds.x -= (editorBounds.getMaxX() - screenSize.width); } if (editorBounds.x < 0) { editorBounds.x = 0; } if (editorBounds.y < 0) { editorBounds.y = 0; } codeGemEditor.setBounds(editorBounds); // Now show it codeGemEditor.setVisible(true); } else { // Only bother if there's something to do if (isCodeEditorVisible(codeGem)) { // Hide the code editor and move to the back CodeGemEditor codeEditor = getCodeGemEditor(codeGem); codeEditor.setVisible(false); } } } /** * Will close/hide all the code gem editors. */ void hideAllCodeGemEditors() { for (final DisplayedGem displayedGem : getDisplayedGems()) { Gem gem = displayedGem.getGem(); if (gem instanceof CodeGem) { showCodeGemEditor((CodeGem)gem, false); } } } /** * Displays a text field from which a collector's name may be edited * @param collectorGem */ void displayLetNameEditor(CollectorGem collectorGem) { TableTopPanel.EditableGemNameField nameField = getTableTopPanel().new EditableGemNameField(collectorGem.getUnqualifiedName(), collectorGem); getTableTopPanel().add(nameField); // calculate the "right" place to put it Rectangle bodyBounds = getDisplayedGem(collectorGem).getDisplayedGemShape().getBodyBounds(); Rectangle fieldBounds = nameField.getBounds(); int widthDif = bodyBounds.width - fieldBounds.width; int heightDif = bodyBounds.height - fieldBounds.height; int newX = bodyBounds.x + widthDif / 2; int newY = bodyBounds.y + heightDif / 2; nameField.setLocation(newX, newY); nameField.requestFocus(); // ensure the cursor is visible nameField.scrollCaretToVisible(); } /** * Displays a text field from which a code gem's name may be edited * @param codeGem */ void displayCodeNameEditor(CodeGem codeGem) { TableTopPanel.EditableGemNameField nameField = getTableTopPanel().new EditableGemNameField(codeGem.getUnqualifiedName(), codeGem); getTableTopPanel().add(nameField); nameField.setLocation(getEditorPositionForTriangularGem(codeGem, nameField)); nameField.requestFocus(); // ensure the cursor is visible nameField.scrollCaretToVisible(); } /** * Displays an editor for the field extracted by an RecordFieldSelection Gem * @param recordFieldSelectionGem * @return the newly created field editor, or null id the editor has nothing to edit */ JComponent displayRecordFieldSelectionEditor(final RecordFieldSelectionGem recordFieldSelectionGem) { // get correct editor to display final JComponent fieldEditor = RecordFieldSelectionGemFieldNameEditor.makeEditor(recordFieldSelectionGem, this); if (fieldEditor != null) { getTableTopPanel().add(fieldEditor); // display editor fieldEditor.setLocation(getEditorPositionForTriangularGem(recordFieldSelectionGem, fieldEditor)); fieldEditor.requestFocus(); } return fieldEditor; } /** * Get position to place editor for triangular gem field. * @param gem * @param field editor for RecordFieldSelection gem field */ Point getEditorPositionForTriangularGem(Gem gem, JComponent field) { // calculate the "right" place to put it Rectangle bodyBounds = getDisplayedGem(gem).getDisplayedGemShape().getBodyBounds(); Rectangle fieldBounds = field.getBounds(); int heightDif = bodyBounds.height - fieldBounds.height; int newX = bodyBounds.x + DisplayConstants.INPUT_OUTPUT_LABEL_MARGIN * 2;// + dotSpace; int newY = bodyBounds.y + heightDif / 2; return new Point(newX, newY); } /** * Display an editor which the field name may be edited * @param rcGem * @param fieldToRename the field to be renamed */ void displayFieldRenameEditor(RecordCreationGem rcGem, FieldName fieldToRename){ RecordFieldRenameEditor fieldEditor = new RecordFieldRenameEditor(rcGem, fieldToRename, this); if(fieldEditor != null){ getTableTopPanel().add(fieldEditor); // Calculate the location to place the editor int index = rcGem.getFieldIndex(fieldToRename); Point connectPt = getDisplayedGem(rcGem).getDisplayedGemShape().getInputConnectPoint(index); int inBoundWidth = getDisplayedGem(rcGem).getDisplayedGemShape().getInBounds().width; Rectangle fieldBounds = fieldEditor.getBounds(); int newX = connectPt.x + inBoundWidth + DisplayConstants.BEVEL_WIDTH_X + DisplayConstants.INPUT_OUTPUT_LABEL_MARGIN / 2; int newY = connectPt.y - fieldBounds.height / 2 ; fieldEditor.setLocation(new Point(newX, newY)); fieldEditor.requestFocus(); } } /** * Find space underneath the preferred location to insert the gems specified in 'insertions' * @param preferredLocation * @param insertions * @return Point */ Point findSpaceUnderneathFor(Point preferredLocation, DisplayedGem[] insertions) { // Get the bounds of this insertion int j = 0; Rectangle rectangle = null; for (j = 0; j < insertions.length; j++) { if (insertions[j] != null) { rectangle = insertions[j].getBounds(); break; } } for (; j < insertions.length; j ++) { if (insertions[j] != null) { rectangle.add(insertions[j].getBounds()); } } if (rectangle == null) { return preferredLocation; } // the closest location is the preferred location! Point closestLocation = preferredLocation; // The set of displayed gems on the table top Set<DisplayedGem> displayedGems = getDisplayedGems(); // We only want to consider the gems that are not being inserted displayedGems.removeAll(Arrays.asList(insertions)); List<Rectangle> gemsUnderneath = new ArrayList<Rectangle>(); // the farthest right x coordinate in the bounds int rightX = preferredLocation.x + rectangle.width; // sort them in order of their y coordinates for (final DisplayedGem displayedGem : displayedGems) { Rectangle gemBounds = displayedGem.getBounds(); if ((gemBounds.x < rightX && (gemBounds.x + gemBounds.width) > preferredLocation.x) && (gemBounds.y >= preferredLocation.y )) { gemsUnderneath.add(gemBounds); } } // Use this comparator Comparator<Rectangle> boundsComparator = new Comparator<Rectangle>() { /** * @see Comparator#compare(Object, Object) */ public int compare(Rectangle o1, Rectangle o2) { return o1.y - o2.y; } }; // ... use the built in merge-sort function Collections.sort(gemsUnderneath, boundsComparator); int size = gemsUnderneath.size(); if (size > 0) { Rectangle rect = gemsUnderneath.get(0); if ((rect.y - closestLocation.y) >= rectangle.height +10) { return closestLocation; } // Take the first available slot where the gems would fit. for (int i = 0; i < size; i++) { rect = gemsUnderneath.get(i); closestLocation = new Point(closestLocation.x, rect.y + rect.height + 10); if (i == (size - 1)) { break; } else { Rectangle rect2 = gemsUnderneath.get(i + 1); if ((rect2.y - closestLocation.y) >= (rectangle.height + 10)) { break; } } } } return closestLocation; } /** * Get the GemGraph associated with this TableTop * @return GemGraph the gem graph associated with this Manager */ GemGraph getGemGraph() { return gemGraph; } /** * Returns the reference point that used to be the initial origin * @return Point */ Point getOriginalOrigin() { return originalOrigin; } /** * Get TypeCheckInfo for the tabletop. * @return TypeCheckInfo the typecheck info for the current state of the tabletop */ TypeCheckInfo getTypeCheckInfo() { return gemCutter.getTypeCheckInfo(); } /** * Get the ModuleTypeInfo for the current module. * @return ModuleTypeInfo. */ ModuleTypeInfo getCurrentModuleTypeInfo() { return getTypeCheckInfo().getModuleTypeInfo(); } /** * Get the intellicut manager. * @return IntellicutManager the intellicut manager. */ IntellicutManager getIntellicutManager() { return gemCutter.getIntellicutManager(); } /** * @return the table top panel that displays the table top */ public TableTopPanel getTableTopPanel() { if (tableTopPanel == null) { tableTopPanel = new TableTopPanel(this, gemCutter); } return tableTopPanel; } /** * Set the running state of the tabletop. * @param running boolean whether or not we are in the run state */ void setRunning(boolean running) { if (running) { // put a gray cast on the tabletop getTableTopPanel().setBackground(Color.lightGray); } else { // not running getTableTopPanel().setBackground(Color.white); } // Disable mouse events if entering the run state (gems shouldn't be allowed to move) // Otherwise enable them. getTableTopPanel().enableMouseEvents(!running); } /** * Ensure the target collector is located correctly for its docked state and the size of the TableTop. */ void checkTargetDockLocation() { // DisplayedGem targetCollector = getTargetDisplayedCollector(); // if (targetCollector == null) { // return; // } // // Get the TableTop bounds and the current location of the TargetGem // Rectangle ttRect = getTableTopPanel().getVisibleRect(); // Point targetLoc = getTargetDisplayedCollector().getLocation(); // if (targetDocked) { // // Make sure its at the dock location // int haloPlusBlur = HALO_SIZE + HALO_BLUR_SIZE; // Point docLoc = new Point(ttRect.width - targetCollector.getDimensions().width - haloPlusBlur, haloPlusBlur); // if (targetLoc.x != docLoc.x || targetLoc.y != docLoc.y) { // // Need to relocate the target and get it to repaint correctly // targetCollector.setLocation(docLoc); // } // } } /** * Save the currently focused component in the table top, if any. */ void saveFocus() { // just store the focus owner into the focus store focusStore = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); } /** * Restore the focus to the stored focused component. */ void restoreFocus() { if (focusStore != null) { // Need special action if we're dealing with focus in a ValueEditor. Component searchAncestor = focusStore; while (searchAncestor != null) { if (searchAncestor instanceof ValueEditor) { gemCutter.getValueEditorHierarchyManager().activateEditor((ValueEditor)searchAncestor); focusStore = null; return; } searchAncestor = searchAncestor.getParent(); } // Else, just request focus. focusStore.requestFocus(); focusStore = null; } } /** * Obtain the undoable edit support object maintained by the tabletop. * This method is intended to be called only by classes which are closely related to the tabletop, so that * they have a way to notify the tabletop that the coming edits can be aggregated, and can be given a * certain name. Normal edits should be posted to the gemCutter's undo manager. * For instance, this is called by the intellicut panel to aggregate intellicut-add gem edits * (add the selected gem, connect it up..) * @return ExtendedUndoableEditSupport the undoable edit support object maintained by the tabletop. */ ExtendedUndoableEditSupport getUndoableEditSupport() { return undoableEditSupport; } /** * Set the cached value node for an input. * @param arg PartInput the input for which to cache the value * @param vn ValueNode the value node to cache. */ void cacheArgValue(PartInput arg, ValueNode vn){ cachedArgValueMap.put(arg, vn); } /** * Get the cached value node for this sink. * @param arg PartInput the input for which a value is cached * @return ValueNode the cached value node or null if none */ final ValueNode getCachedValue(PartInput arg){ return cachedArgValueMap.get(arg); } /** * Clear cached arguments. * @param displayedGem DisplayedGem the gem for which to clear the cached values */ void clearCachedArguments(DisplayedGem displayedGem) { // grab unbound input parts on the tree rooted by the connected gem List<PartInput> inputParts = displayedGem.getTargetArguments(); // iterate over the gathered input parts. for (final PartInput inputPart : inputParts) { // clear the cached type cacheArgValue(inputPart, null); } } /** * Get the type colour for a connectable displayed part * @return Color the type colour */ Color getTypeColour(DisplayedPartConnectable part) { // Colour is black if connected if (part.isConnected()) { return getDefaultLineColor(); } // Return the input type colour TypeColourManager typeColourManager = gemCutter.getTypeColourManager(); return (typeColourManager == null) ? getDefaultLineColor() : typeColourManager.getTypeColour(part.getPartConnectable().getType()); } private Color getDefaultLineColor() { return getTableTopPanel().isPhotoLook() ? Color.BLACK : SystemColor.textText; } /** * Update the displayed connections for a gem. Unpaint any old connections, paint new ones. * @param displayedGem DisplayedGem the gem whose connections to repaint */ private final void updateDisplayedConnections(DisplayedGem displayedGem) { List<DisplayedPartConnectable> connectableParts = displayedGem.getConnectableDisplayedParts(); for (final DisplayedPartConnectable part : connectableParts) { DisplayedConnection displayedConnection = part.getDisplayedConnection(); if (displayedConnection != null) { // unpaint the old connection (recall: painting performs XOR's) if any part.repaintConnection(getTableTopPanel()); // clear any cached connection part.purgeConnectionRoute(); // recalculates a new connection and repaints it (if any) part.repaintConnection(getTableTopPanel()); } } } /** * Get a new ConnectionRoute between the two given parts. * @param from DisplayedPartConnectable the source part * @param to DisplayedPartConnectable the destination part * @return ConnectionRoute the new route */ static ConnectionRoute getConnectionRoute(DisplayedPartConnectable from, DisplayedPartConnectable to) { // Get the start and end points for the route return new ConnectionRoute(from.getConnectionPoint(), to.getConnectionPoint()); } /** * Get the bounds of the GemGraph. * Note: this method iterates over all the gems (ie. O(n) in the number of gems..) * @return Rectangle the bounds of the GemGraph. */ private Rectangle getGemGraphBounds() { return getGemBounds(getDisplayedGems()); } /** * Get the bounds of the selected gems * Note: this method iterates over all the gems (ie. O(n) in the number of gems..) * @return Rectangle the bounds of the selected gems */ private Rectangle getSelectedGemsBounds() { return getGemBounds(Arrays.asList(getSelectedDisplayedGems())); } /** * Get the bounds of the specified gems * Note: this method iterates over all the gems (ie. O(n) in the number of gems..) * @param gems the gems to include in the bound * @return Rectangle the bounds of the gems */ private Rectangle getGemBounds(Collection<DisplayedGem> gems) { // Declare the rectangle Rectangle rect = null; if (!gems.isEmpty()){ for (final DisplayedGem displayedGem : gems) { if (rect == null){ rect = displayedGem.getBounds(); } else { rect.add(displayedGem.getBounds()); } } } else { // No gems - return a trivial rectangle. Maybe this should never happen? rect = new Rectangle(); } return rect; } /** * Determine the Gem's bounds when selected. * * @param displayedGem the gem for which to calculate bounds * @return Rectangle the bounds of the Gem when selected */ private static Rectangle getSelectedBounds(DisplayedGem displayedGem) { return getSelectedBounds(displayedGem, displayedGem.getBounds()); } /** * Determine the Gem's bounds when selected. * This calculates the amount by which a set of bounds must be expanded, depending on the * type of gem which is passed in. * * Note that Gems whose selection 'halos' lie entirely within one or more of the dimensions of * their overall bounds (getBounds()) will not need to expand the selection rectangle in those * dimensions. For instance an InputOutputGem has provision for both an input and output * area and hence only needs to grow in the Y dimension. * * @param displayedGem the gem for which to calculate bounds * @param bounds the bounds from which to calculate. * @return Rectangle the bounds of the Gem when selected */ private static Rectangle getSelectedBounds(DisplayedGem displayedGem, Rectangle bounds) { Gem gem = displayedGem.getGem(); // Get the regular bounds and add a contribution to the width and height caused by the 'glow' Rectangle selBounds = new Rectangle(bounds); selBounds.y -= DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE; selBounds.height += (DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE) * 2; // Grow to the right if there is no output if (gem.getOutputPart() == null) { selBounds.width += DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE; } // Grow to the left if there are no inputs if (gem.getInputParts().length == 0) { selBounds.x -= DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE; selBounds.width += DisplayConstants.HALO_SIZE + DisplayConstants.HALO_BLUR_SIZE; } return selBounds; } /** * Add an undoable edit listener. * @param uel */ public void addUndoableEditListener(javax.swing.event.UndoableEditListener uel) { undoableEditSupport.addUndoableEditListener(uel); } /** * Remove an undoable edit listener. * @param uel */ public void removeUndoableEditListener(javax.swing.event.UndoableEditListener uel) { undoableEditSupport.removeUndoableEditListener(uel); } /** * Adds the specified state change listener to receive state change events from this gem . * If l is null, no exception is thrown and no action is performed. * * @param l the state listener. */ public synchronized void addStateChangeListener(DisplayedGemStateListener l) { if (l == null) { return; } gemStateListener = GemEventMulticaster.add(gemStateListener, l); } /** * Removes the specified state change listener so that it no longer receives state change events from this gem. * This method performs no function, nor does it throw an exception, if the listener specified by * the argument was not previously added to this component. * If l is null, no exception is thrown and no action is performed. * * @param l the state listener. */ public synchronized void removeStateChangeListener(DisplayedGemStateListener l) { if (l == null) { return; } gemStateListener = GemEventMulticaster.remove(gemStateListener, l); } /** * Adds the specified connection state listener to receive connection state events from this input. * If l is null, no exception is thrown and no action is performed. * * @param l the connection state listener. */ public synchronized void addConnectionStateListener(DisplayedConnectionStateListener l) { if (l == null) { return; } connectionStateListener = GemEventMulticaster.add(connectionStateListener, l); } /** * Removes the specified connection state listener so that it no longer receives connection state events. * This method performs no function, nor does it throw an exception, if the listener specified by * the argument was not previously added to this component. * If l is null, no exception is thrown and no action is performed. * * @param l the connection state listener */ public synchronized void removeConnectionStateListener(DisplayedConnectionStateListener l) { if (l == null) { return; } connectionStateListener = GemEventMulticaster.remove(connectionStateListener, l); } /* * Methods for actions ******************************************** */ /** * Add a gem. * @param displayedGem DisplayedGem the gem to add * @param addPoint Point the location to add the gem */ void addGem(DisplayedGem displayedGem, Point addPoint) { // Set the location of the Gem displayedGem.setLocation(addPoint); // Add the Gem to the Gem graph. Gem gemModel = displayedGem.getGem(); if (displayedGem != targetDisplayedCollector) { gemGraph.addGem(gemModel); } // Now handle the creation of the displayed gem, and adding of the gem to the gem graph. handleDisplayedGemAdded(displayedGem); // TODOEL: TEMP. Reassign the collector's input argument to be the target, if this is a new gem. if (gemModel instanceof CollectorGem) { CollectorGem collectorGem = (CollectorGem)gemModel; Gem.PartInput collectingPart = collectorGem.getCollectingPart(); if (collectorGem != getTargetCollector() && collectorGem.getTargetArguments().contains(collectingPart)) { retargetInputArgument(collectingPart, getTargetCollector(), -1); } } } /** * Add a gem and update the tabletop for the addition. * @param displayedGem DisplayedGem the gem to add * @param addPoint Point the location to add the gem */ private void addGemAndUpdate(DisplayedGem displayedGem, Point addPoint) { addGem(displayedGem, addPoint); // Update the tabletop for the new gem graph state. updateForGemGraph(); resizeForGems(); } /** * Handle the addition of a displayed gem to the tabletop. * This method performs setup / initialization of tabletop data structures. * @param displayedGem the displayed gem being added. */ private void handleDisplayedGemAdded(DisplayedGem displayedGem) { // Add the Gem to the display map. Gem gemModel = displayedGem.getGem(); gemDisplayMap.put(gemModel, displayedGem); // Get the TableTop to repaint the area where we added a Gem if (displayedGem != targetDisplayedCollector) { getTableTopPanel().repaint(getSelectedBounds(displayedGem)); } // Clear the GemCutter's adding gem if it had any. gemCutter.setAddingDisplayedGem(null); // add the event listener as a listener on various gem events displayedGem.addLocationChangeListener(gemEventListener); displayedGem.addSizeChangeListener(gemEventListener); displayedGem.addLocationChangeListener(getTableTopPanel().getGemPainter()); displayedGem.addSizeChangeListener(getTableTopPanel().getGemPainter()); gemModel.addBurnListener(gemEventListener); gemModel.addInputChangeListener(gemEventListener); gemModel.addNameChangeListener(gemEventListener); gemModel.addStateChangeListener(gemEventListener); gemModel.addTypeChangeListener(gemEventListener); gemModel.addStateChangeListener(gemCutter.getTargetRunnableListener()); // Special handling for Value Gems (need to display its valueEntryPanel) if (gemModel instanceof ValueGem) { tableTopPanel.handleValueGemAdded((ValueGem)gemModel); } else if (gemModel instanceof CodeGem) { CodeGem codeGem = (CodeGem)gemModel; codeGem.addDefinitionChangeListener(gemEventListener); // Create a code gem editor if there isn't one already. CodeGemEditor codeGemEditor = codeGemEditorMap.get(codeGem); if (codeGemEditor == null) { codeGemEditor = getNewCodeGemEditor(codeGem); codeGemEditorMap.put(codeGem, codeGemEditor); unplacedCodeGemEditorMap.put(codeGem, codeGemEditor); } } } /** * Handle the situation where the value gem's value editor has had its value committed. * Other value gems will be type switched as necessary, and updates will be posted to the undo manager. * This method does not make any assumptions about whether the value gem in question has already had its value updated. * No action will be taken if the old and new values are the same value. * @param committedValueGem the value gem in question * @param oldValue the value before the change. * @param newValue the value after the change. */ void handleValueGemCommitted(ValueGem committedValueGem, ValueNode oldValue, ValueNode newValue) { // check for nothing to do. if (oldValue.sameValue(newValue)) { return; } undoableEditSupport.beginUpdate(); // Get the new types of changed value gems. Map<ValueGem, ValueNode> valueGemToNewValueMap = gemGraph.getValueGemSwitchValues(committedValueGem, oldValue, newValue, getTypeCheckInfo()); // Update the valuegems with their new types and post any value edits to the undo manager. boolean valueGemChangesPosted = false; for (final Map.Entry<ValueGem, ValueNode> mapEntry : valueGemToNewValueMap.entrySet()) { ValueGem valueGemChanged = mapEntry.getKey(); ValueNode valueGemChangedValue = mapEntry.getValue(); ValueNode preChangeValue = (valueGemChanged == committedValueGem) ? oldValue : valueGemChanged.getValueNode(); if (!preChangeValue.sameValue(valueGemChangedValue)) { valueGemChanged.changeValue(valueGemChangedValue); undoableEditSupport.postEdit(new UndoableValueChangeEdit(valueGemChanged, preChangeValue)); valueGemChangesPosted = true; } } // Post edits if any. if (valueGemChangesPosted) { // Update the tabletop for the new gem graph state. updateForGemGraph(); undoableEditSupport.endUpdate(); } else { undoableEditSupport.endUpdateNoPost(); } } /** * Update the code gems and their editors with respect to any text or type changes. */ void updateCodeGemEditors() { for (final CodeGemEditor codeGemEditor : codeGemEditorMap.values()) { codeGemEditor.doSyntaxSmarts(); updateForConnectivity(codeGemEditor); } } /** * Reset the tabletop to be completely clean. */ private void blankTableTop() { // Remove all gems. This also does auxiliary clean up such as hiding code panels. Set<Gem> gemSet = gemGraph.getGems(); doDeleteGemsUserAction(gemSet); // Update the tabletop for the new gem graph state. updateForGemGraph(); // reinitialize some members cachedArgValueMap.clear(); getTableTopPanel().resetState(); // reset the metadata for the table top target collector FunctionMetadata oldMetadata = getTargetCollector().getDesignMetadata(); getTargetCollector().clearDesignMetadata(); // post an undoable edit for this reseting of the metadata getUndoableEditSupport().postEdit(new UndoableChangeCollectorDesignMetadataEdit(this, getTargetCollector(), oldMetadata)); } /** * Change a gem's location. * @param displayedGem the gem to move * @param newLocation the new location of the gem. */ void changeGemLocation(DisplayedGem displayedGem, Point newLocation) { // Change the gem location displayedGem.setLocation(newLocation); } /** * Make a connection. * @param srcPart the source part * @param destPart the destination part */ void connect(PartOutput srcPart, PartInput destPart) { // Defer to the other connect(). connect(new Connection(srcPart, destPart)); } /** * Make a connection * @param connection the connection to make. */ private void connect(Connection connection) { // Make the connection gemGraph.connectGems(connection); DisplayedConnection displayedConnection = handleConnectionAdded(connection); // repaint parts getTableTopPanel().repaint(displayedConnection.getBounds()); getTableTopPanel().repaint(displayedConnection.getSource().getBounds()); getTableTopPanel().repaint(displayedConnection.getDestination().getBounds()); } /** * Handle the addition of a connection to the tabletop. * This method performs setup / initialization of tabletop data structures. * @param connection the connection being added. * @return DisplayedConnection the displayed connection which was created. */ private DisplayedConnection handleConnectionAdded(Connection connection) { // Create a new displayed connection. DisplayedPartOutput dSource = (DisplayedPartOutput)getDisplayedPartConnectable(connection.getSource()); DisplayedPartInput dDest = (DisplayedPartInput)getDisplayedPartConnectable(connection.getDestination()); DisplayedConnection displayedConnection = new DisplayedConnection(dSource, dDest); // bind connections dSource.bindDisplayedConnection(displayedConnection); dDest.bindDisplayedConnection(displayedConnection); // Add to the map. connectionDisplayMap.put(displayedConnection.getConnection(), displayedConnection); // Add the connection listener addConnectionStateListener(gemEventListener); return displayedConnection; } /** * Update the given code gem editor's state according to the validity of its connections. * @param codeGemEditor */ private void updateForConnectivity(CodeGemEditor codeGemEditor) { codeGemEditor.updateForConnectivity(getTypeCheckInfo(), gemCutter.getValueEditorManager()); } /** * Get the map from value gem to its current value node. * @return Map from value gem to its value node. */ private Map<ValueGem, ValueNode> getValueGemToValueMap() { Map<ValueGem, ValueNode> valueGemToValueMap = new HashMap<ValueGem, ValueNode>(); for (final Gem gem : gemGraph.getGems()) { if (gem instanceof ValueGem) { ValueGem valueGem = (ValueGem)gem; valueGemToValueMap.put(valueGem, valueGem.getValueNode()); } } return valueGemToValueMap; } /** * Given a map from value gem to its former value node, post value change edits to the undo manager for any values that changed. * @param valueGemToOldValueMap Map from value gem to its former value node. * @return boolean whether any value gem changes were posted. */ private boolean postValueGemChanges(Map<ValueGem, ValueNode> valueGemToOldValueMap) { boolean valueGemChangesPosted = false; // Post a ValueChangeEdit for each value gem that changed. for (final Map.Entry<ValueGem, ValueNode> mapEntry : valueGemToOldValueMap.entrySet()) { ValueGem valueGem = mapEntry.getKey(); ValueNode oldValue = mapEntry.getValue(); if (!oldValue.sameValue(valueGem.getValueNode())) { undoableEditSupport.postEdit(new UndoableValueChangeEdit(valueGem, oldValue)); valueGemChangesPosted = true; } } return valueGemChangesPosted; } /** * Perform the necessary steps resulting from a user-initiated disconnect action. * @param connection the connection to disconnect */ void disconnect(Connection connection) { // do the disconnection gemGraph.disconnectGems(connection); // remove from map DisplayedConnection displayedConnection = connectionDisplayMap.remove(connection); // disconnect the displayed parts displayedConnection.getSource().bindDisplayedConnection(null); displayedConnection.getDestination().bindDisplayedConnection(null); // Repaint the disconnected bounds. getTableTopPanel().repaint(displayedConnection.getBounds()); getTableTopPanel().repaint(displayedConnection.getSource().getBounds()); getTableTopPanel().repaint(displayedConnection.getDestination().getBounds()); } /** * Perform the steps necessary to result in a split connection action with the specified collector * and emitter gems. * @param conn the connection to be split * @param collGem new collector gem to connect to the original connection's src part * @param emitGem new emitter gem to connect to the original connection's dest part */ void splitConnectionWith(Connection conn, DisplayedGem collGem, DisplayedGem emitGem){ DisplayedConnection displayConn = connectionDisplayMap.get(conn); // Disconnect without updating arguments in gemGraph gemGraph.setArgumentUpdatingDisabled(true); disconnect(conn); // Add the collector and emitter gems to tabletop addGem(collGem, displayConn.getSourceConnectionPoint()); addGem(emitGem, displayConn.getTargetConnectionPoint()); tidyNewGemPair(displayConn, collGem, emitGem); connect(conn.getSource(), collGem.getGem().getInputPart(0)); connect(emitGem.getGem().getOutputPart(), conn.getDestination()); gemGraph.setArgumentUpdatingDisabled(false); updateForGemGraph(); resizeForGems(); } /** * Place the new collector/emitter gem pair as close to the 2 existing gems as possible or * further apart if the gems overlap or look out of place(ie. the collector is to the right of the emitter * within the lengths of the gems). Existing gems are anchored. * Note: This method cannot guarantee the new gems will not overlap with other gems on the table top. * * @param displayConn the original connection to be split * @param newCollectorGem new collector gem * @param newEmitterGem new emitter gem */ private void tidyNewGemPair(DisplayedConnection displayConn, DisplayedGem newCollectorGem, DisplayedGem newEmitterGem) { // Location of the connection points final Point srcPos = displayConn.getSourceConnectionPoint(); final Point destPos = displayConn.getTargetConnectionPoint(); final Rectangle collectorRect = newCollectorGem.getBounds(); final Rectangle emitterRect = newEmitterGem.getBounds(); // Initially try to place the new gems close to the existing gems. These positions maybe change below. Point collGemPosition = new Point(srcPos.x + GAP_BETWEEN_GEMS, srcPos.y - collectorRect.height / 2); Point emitGemPosition = new Point(destPos.x - GAP_BETWEEN_GEMS * 6, destPos.y - emitterRect.height / 2); collectorRect.setLocation(collGemPosition); emitterRect.setLocation(emitGemPosition); // (x,y) of the rectangle's top left corner (top left corner of the table top is (0,0)) int colRectX = collectorRect.x; int colRectY = collectorRect.y; int emitRectX = emitterRect.x; int emitRectY = emitterRect.y; int yDiff = Math.abs(colRectY - emitRectY); // If the new gems overlap or look out of place (collector gem to the right of the emitter gem when there is overlapping // of the gems' lengths), rearrange them if (collectorRect.intersects(emitterRect) || (colRectX > emitRectX && yDiff < collectorRect.height)) { // Stack the gems vertically int avgY = (colRectY + emitRectY) / 2; int avgX = (colRectX + emitRectX) / 2; colRectX = avgX; emitRectX = avgX; // See which new gem should be placed on top if (colRectY < emitRectY) { colRectY = avgY - GAP_BETWEEN_GEMS / 2 - collectorRect.height; emitRectY = avgY + GAP_BETWEEN_GEMS / 2; } else { emitRectY = avgY - GAP_BETWEEN_GEMS / 2 - emitterRect.height; colRectY = avgY + GAP_BETWEEN_GEMS / 2; } //--------- If limited horizontal space, try moving the pair further away from the source and destination gems // Moving the collector gem towards the right and above or below the destination gem int connSourceX = displayConn.getSourceConnectionPoint().x; if (avgX < connSourceX) { colRectX = connSourceX + GAP_BETWEEN_GEMS; Rectangle destGem = displayConn.getDestination().getDisplayedGem().getBounds(); // Move the collector gem above the destination gem if it is already higher, else move it below if (colRectY < emitRectY) { colRectY = destGem.y - collectorRect.height - GAP_BETWEEN_GEMS; } else { colRectY = destGem.y + destGem.height + GAP_BETWEEN_GEMS; } } // Moving the emitter gem towards the left and try to move above or below the source gem if (avgX + emitterRect.width > displayConn.getTargetConnectionPoint().x) { emitRectX = displayConn.getTargetConnectionPoint().x - emitterRect.width - GAP_BETWEEN_GEMS; Rectangle srcGem = displayConn.getSource().getDisplayedGem().getBounds(); // Move the emitter gem below the source gem if it is already lower, else move it above if (colRectY < emitRectY) { emitRectY = srcGem.y + srcGem.height + GAP_BETWEEN_GEMS; } else { emitRectY = srcGem.y - emitterRect.height - GAP_BETWEEN_GEMS; } } // Set the new location for the pair collGemPosition = new Point(colRectX, colRectY); emitGemPosition = new Point(emitRectX, emitRectY); } changeGemLocation(newEmitterGem, emitGemPosition); changeGemLocation(newCollectorGem, collGemPosition); } /** * Remove a given gem from the tabletop. * @param gemToDelete the gem to remove */ void deleteGem(Gem gemToDelete) { DisplayedGem displayedGem = gemDisplayMap.get(gemToDelete); // Clear the focused displayed gem if necessary if (displayedGem == getFocusedDisplayedGem()) { setFocusedDisplayedGem(null); } // Hide any code gem editor if (gemToDelete instanceof CodeGem) { showCodeGemEditor(((CodeGem)gemToDelete), false); ((CodeGem)gemToDelete).removeDefinitionChangeListener(gemEventListener); } // Notify the tableTop panel if a value gem was deleted. if (gemToDelete instanceof ValueGem) { tableTopPanel.handleValueGemRemoved((ValueGem)gemToDelete); } // Now actually delete the gems. gemGraph.removeGem(gemToDelete); gemDisplayMap.remove(gemToDelete); selectedDisplayedGems.remove(gemToDelete); runningDisplayedGems.remove(gemToDelete); // Update any menu buttons which depend on displayed gems checkCopySpecialMenu(); // Repaint the gem area without the gem getTableTopPanel().repaint(getSelectedBounds(displayedGem)); // remove the event listener as a listener on various gem events displayedGem.removeLocationChangeListener(gemEventListener); displayedGem.removeSizeChangeListener(gemEventListener); displayedGem.removeLocationChangeListener(getTableTopPanel().getGemPainter()); displayedGem.removeSizeChangeListener(getTableTopPanel().getGemPainter()); gemToDelete.removeBurnListener(gemEventListener); gemToDelete.removeInputChangeListener(gemEventListener); gemToDelete.removeNameChangeListener(gemEventListener); gemToDelete.removeStateChangeListener(gemEventListener); gemToDelete.removeTypeChangeListener(gemEventListener); gemToDelete.removeStateChangeListener(gemCutter.getTargetRunnableListener()); } /** * Moves all gems x distance and y distance. * Note: Also invalidates all previous connections (since they're now off). * @param x int * @param y int */ void moveAllGems(int x, int y) { // Consider each Gem for (final DisplayedGem displayedGem : getDisplayedGems()) { Point oldPT = displayedGem.getLocation(); Point newPT = new Point(oldPT.x + x, oldPT.y + y); changeGemLocation(displayedGem, newPT); } // The connections routes are invalid now. for (final DisplayedConnection displayedConnection : connectionDisplayMap.values()) { displayedConnection.purgeRoute(); } // move all the value entry panels (temporarily disabled - todoKW: fix VEP painting bug) // moveVEPs(x, y); Point newOrigin = new Point(originalOrigin.x + x, originalOrigin.y + y); originalOrigin = newOrigin; } /** * Find out whether or not the given gem is running. * @param displayedGem * @return whether or not the given gem is running. */ public final boolean isRunning(DisplayedGem displayedGem){ return runningDisplayedGems.contains(displayedGem); } /** * Set the running state of a given gem. * @param displayedGem the gem in question. * @param newState the new running state of the gem. */ void setRunning(DisplayedGem displayedGem, boolean newState){ boolean running = isRunning(displayedGem); if (running != newState) { if (newState) { runningDisplayedGems.add(displayedGem); } else { runningDisplayedGems.remove(displayedGem); } if (gemStateListener != null) { gemStateListener.runStateChanged( new DisplayedGemStateEvent(displayedGem, displayedGem.getBounds(), DisplayedGemStateEvent.EventType.RUNNING)); } } } /** * Set the bad state of a connection * @param newState the new bad state of the Gem */ final void setBadDisplayedConnection(DisplayedConnection displayedConnection, boolean newState) { final boolean selected = isBadDisplayedConnection(displayedConnection); if (selected != newState) { if (newState) { badDisplayedConnections.add(displayedConnection); } else { badDisplayedConnections.remove(displayedConnection); } if (connectionStateListener != null) { connectionStateListener.badStateChanged( new DisplayedConnectionStateEvent(displayedConnection, displayedConnection.getBounds(), DisplayedConnectionStateEvent.EventType.BAD)); } } } /** * Return whether the given connection is currently considered bad. * @param displayedConnection the connection in question * @return whether the given connection is currently considered bad. */ public final boolean isBadDisplayedConnection(DisplayedConnection displayedConnection) { return badDisplayedConnections.contains(displayedConnection); } /** * Set the selection state of this Gem * @param newState the new selection state of the Gem */ private final void setSelectionState(DisplayedGem displayedGem, boolean newState) { final boolean selected = isSelected(displayedGem); if (selected != newState) { if (newState) { selectedDisplayedGems.add(displayedGem); } else { selectedDisplayedGems.remove(displayedGem); } if (gemStateListener != null) { gemStateListener.selectionStateChanged( new DisplayedGemStateEvent(displayedGem, displayedGem.getBounds(), DisplayedGemStateEvent.EventType.SELECTION)); } } } /** * Toggle the selection state of this Gem * @return the resulting state of the Gem (after toggling) */ final boolean toggleSelected(DisplayedGem displayedGem) { boolean selected = !isSelected(displayedGem); setSelectionState(displayedGem, selected); checkSelectionButtons(); return selected; } /** * Return whether the given gem is currently selected. * @param displayedGem the gem in question * @return whether the given gem is currently selected. */ public final boolean isSelected(DisplayedGem displayedGem) { return selectedDisplayedGems.contains(displayedGem); } /** * Select the given Gem in the UI, or deselect all Gems. * @param gem the Gem to select (or null to deselect all) * @param singleton whether to deselect all other Gems (true) or select this Gem in addition to others */ void selectGem(Gem gem, boolean singleton) { selectDisplayedGem(getDisplayedGem(gem), singleton); } /** * Select the given Gem in the UI, or deselect all Gems. * @param displayedGem the displayed Gem to select (or null to deselect all) * @param singleton whether to deselect all other Gems (true) or select this Gem in addition to others */ void selectDisplayedGem(DisplayedGem displayedGem, boolean singleton) { // If gem is null, or singleton is set, we deselect everything if (singleton || (displayedGem == null)) { // Deselect each Gem in the graph for (final DisplayedGem dGem : getDisplayedGems()){ if (isSelected(dGem)) { setSelectionState(dGem, false); } } } // Now select this Gem if (displayedGem != null) { setSelectionState(displayedGem, true); } checkSelectionButtons(); } /** * Goes thru the list of gems, and if a gem is not selected, then selects it. * Note: The target gem will only be considered for selection if it's undocked. */ void selectAllGems() { // We want to give focus to the Gem nearest the top left corner of the TableTop // so keep track of which is closest as we select each one. double dist = -1; DisplayedGem gemToFocusOn = null; for (final DisplayedGem thisGem : getDisplayedGems()){ setSelectionState(thisGem, true); // Do the distance checking here. double thisDist = thisGem.getCenterPoint().distance(0, 0); if (dist < 0 || thisDist < dist) { dist = thisDist; gemToFocusOn = thisGem; } } checkSelectionButtons(); // Actually focus on a Gem here setFocusedDisplayedGem(gemToFocusOn); } /** * Select gems in the UI according to the select area and select mode. * @param hitRect the select area * @param selectMode SelectMode the select mode */ void selectGems(Rectangle2D hitRect, TableTopPanel.SelectMode selectMode) { // If the select mode is replace, first deselect all gems if (selectMode == TableTopPanel.SelectMode.REPLACE_SELECT) { selectDisplayedGem(null, true); } // For each DisplayedGem, check for intersection with the hitRect and // change its selection state accordingly for (final DisplayedGem displayedGem : getDisplayedGems()) { // The gem is hit if its center point lies within the hit rectangle if (hitRect.contains(displayedGem.getCenterPoint())) { if (selectMode == TableTopPanel.SelectMode.TOGGLE) { // Toggle state toggleSelected(displayedGem); } else { // Select setSelectionState(displayedGem, true); } } } checkSelectionButtons(); } /** * Ensures that buttons dependent on gem selection are in their proper * state (eg: copy/cut/delete are enabled when gems are selected) */ void checkSelectionButtons() { checkCutCopyDeleteValid(); checkCopySpecialMenu(); } /** * Checks whether something has been selected and enables/disables * the copy, cut and delete buttons */ void checkCutCopyDeleteValid() { boolean itemsSelected = !selectedDisplayedGems.isEmpty(); gemCutter.getCutAction().setEnabled(itemsSelected); gemCutter.getCopyAction().setEnabled(itemsSelected); gemCutter.getDeleteAction().setEnabled(itemsSelected); } /** * Checks whether something has been selected, and renames and enables the * "Copy Special" submenu items accordingly. */ void checkCopySpecialMenu() { // Determine gems affected by the "Copy As" menu (ie: selected gems, // or all tabletop displayed gems) Set<DisplayedGem> copySpecialGems; if (!selectedDisplayedGems.isEmpty()) { // The menu will look at selected gems copySpecialGems = selectedDisplayedGems; gemCutter.getCopySpecialImageAction().putValue(Action.NAME, GemCutter.getResourceString("CopySpecialSelectionImage")); } else { // The menu will look at all tabletop gems copySpecialGems = getDisplayedGems(); gemCutter.getCopySpecialImageAction().putValue(Action.NAME, GemCutter.getResourceString("CopySpecialTableTopImage")); } // Now determine if any code gems are included in the set of gems, and enable/disable // the Copy As Text button boolean includesCodeGems = false; for (final DisplayedGem displayedGem : copySpecialGems) { if (displayedGem.getGem() instanceof CodeGem) { includesCodeGems = true; break; } } gemCutter.getCopySpecialTextAction().setEnabled(includesCodeGems); } /** * Completes the actions necessary to rename a codeGem * @param codeGem * @param newName */ void renameCodeGem(CodeGem codeGem, String newName) { codeGem.setName(newName); // and update the title in the editor CodeGemEditor codeGemEditor = getCodeGemEditor(codeGem); String newEditorTitle = GemCutterMessages.getString("CGE_EditorTitle", newName); codeGemEditor.setName(newEditorTitle); codeGemEditor.setTitle(newEditorTitle); // redraw the gem getTableTopPanel().repaint(); } /** * Ensure the TableTop is sized appropriately for the gems which appear on it. * For now this only ensures that it's big enough - no cropping is performed. */ void resizeForGems() { // make sure it's not the (docked) target that resizes the TableTop checkTargetDockLocation(); // get the GemGraph bounds Rectangle gemGraphBounds = getGemGraphBounds(); int xMaxGemGraph = gemGraphBounds.x + gemGraphBounds.width; int yMaxGemGraph = gemGraphBounds.y + gemGraphBounds.height; // get the current tabletop bounds java.awt.Dimension currentBounds = getTableTopPanel().getSize(); int xMaxCurrent = currentBounds.width; int yMaxCurrent = currentBounds.height; // beyond bottom or right edge? if (xMaxGemGraph > xMaxCurrent || yMaxGemGraph > yMaxCurrent) { int xMaxNew = Math.max(xMaxGemGraph, xMaxCurrent); int yMaxNew = Math.max(yMaxGemGraph, yMaxCurrent); java.awt.Dimension newSize = new java.awt.Dimension(xMaxNew, yMaxNew); getTableTopPanel().setSize(newSize); getTableTopPanel().setPreferredSize(newSize); // triggers revalidation } // beyond top or left edge? if (gemGraphBounds.x < 0 || gemGraphBounds.y < 0) { Rectangle visibleRect = getTableTopPanel().getVisibleRect(); int moveDistanceX = Math.max(0, -(gemGraphBounds.x)); int moveDistanceY = Math.max(0, -(gemGraphBounds.y)); moveAllGems(moveDistanceX, moveDistanceY); java.awt.Dimension newSize = new java.awt.Dimension(getTableTopPanel().getWidth() + moveDistanceX, getTableTopPanel().getHeight() + moveDistanceY); getTableTopPanel().setSize(newSize); getTableTopPanel().setPreferredSize(newSize); // scroll back the old bottom right corner so we don't "jump" the viewport Rectangle newVisibleRect = new Rectangle(visibleRect.width + moveDistanceX, visibleRect.height + moveDistanceY, 0, 0); getTableTopPanel().scrollRectToVisible(newVisibleRect); } } /* * Methods for user actions ******************************************** */ /** * Completes the work necessary to do a user copy action. */ void doCopyUserAction() { /// Take the selected gems and make a copy on the clipboard DisplayedGem selectedDisplayedGems[] = getSelectedDisplayedGems(); Clipboard clipboard = gemCutter.getClipboard(); DisplayedGemSelection displayedGemSelection = new DisplayedGemSelection(selectedDisplayedGems, gemCutter); clipboard.setContents(displayedGemSelection, displayedGemSelection); // Now we know the paste action is possible gemCutter.getPasteAction().setEnabled(true); } /** * Takes a snapshot of the whole tabletop and stores this as an image * on the system clipboard. */ void doCopySpecialImageAction() { TableTopPanel tableTopPanel = TableTop.this.getTableTopPanel(); // Determine the bounds of the snapshot area Rectangle bounds; if (getSelectedGems().length > 0) { // We have a selection, so will image this area plus a border bounds = getSelectedGemsBounds(); int dx = Math.max(0, bounds.x - SNAPSHOT_BOUNDARY_PAD) - bounds.x; int dy = Math.max(0, bounds.y - SNAPSHOT_BOUNDARY_PAD) - bounds.y; bounds.x += dx; // move origin bounds.width -= dx; // and widen size bounds.y += dy; bounds.height -= dy; bounds.width = Math.min(bounds.x + bounds.width + SNAPSHOT_BOUNDARY_PAD, tableTopPanel.getWidth()) - bounds.x; bounds.height = Math.min(bounds.y + bounds.height + SNAPSHOT_BOUNDARY_PAD, tableTopPanel.getHeight()) - bounds.y; } else { // No selection, so image the whole table top bounds = new Rectangle(0, 0, tableTopPanel.getWidth(), tableTopPanel.getHeight()); } // Take snapshot of the required area into a new image BufferedImage newImage = new BufferedImage( bounds.width + 1, bounds.height + 1, BufferedImage.TYPE_INT_ARGB_PRE ); Graphics2D g2d = newImage.createGraphics(); g2d.setClip(new Rectangle(0, 0, bounds.width, bounds.height)); g2d.translate(-bounds.x + 1, -bounds.y + 1); tableTopPanel.paintComponent(g2d); // Put a border around the image g2d.setColor(Color.BLACK); g2d.setStroke(new BasicStroke(2)); g2d.translate(bounds.x - 1, bounds.y - 1); g2d.drawRect(0, 0, bounds.width, bounds.height); g2d.dispose(); // Send this to the clipboard Transferable imageTransferable = new ImageTransferable(newImage); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(imageTransferable, null); // For platforms which have a selection clipboard (such as X11), store the image there also clipboard = Toolkit.getDefaultToolkit().getSystemSelection(); if (clipboard != null) { clipboard.setContents(imageTransferable, null); } } /** * Copies all code from tabletop code gems, as HTML text * on the system clipboard. */ void doCopySpecialTextAction() { // Select gems and order by name List<DisplayedGem> gemList = new ArrayList<DisplayedGem>(); gemList.addAll(Arrays.asList(getSelectedDisplayedGems())); if (gemList.size() == 0) { // If no gems are selected, will look through all gems gemList.addAll(getDisplayedGems()); } Collections.sort(gemList, new DisplayedGemComparator()); // Build html string with contents of selected code gems boolean cgFound = false; String htmlText = "<html><body>"; for (final DisplayedGem displayedGem : gemList) { if (displayedGem.getGem() instanceof CodeGem) { CodeGem gem = (CodeGem)displayedGem.getGem(); htmlText += "\n<p><b>" + HtmlHelper.htmlEncode(gem.getUnqualifiedName()) + ":</b><br>"; htmlText += HtmlHelper.htmlEncode(gem.getVisibleCode()).replaceAll("\n", "<br>").replaceAll(" "," "); htmlText += "</p><br><br>"; cgFound = true; } } htmlText += "</body></html>"; // Send text to clipboard if (!cgFound) { return; } Transferable htmlTransferable = new HtmlTransferable(htmlText); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(htmlTransferable, null); // For platforms which have a selection clipboard (such as X11), store the text there also clipboard = Toolkit.getDefaultToolkit().getSystemSelection(); if (clipboard != null) { clipboard.setContents(htmlTransferable, null); } } /** * Copies the source for the target gem to the clipboard. */ void doCopySpecialTargetSourceAction() { // Get target source text Transferable stringTransferable = new StringSelection(targetDisplayedCollector.getTarget().getTargetDef(null, gemCutter.getPerspective().getWorkingModuleTypeInfo()).toString().replaceAll("\r\n","\n")); // Send text to clipboard Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringTransferable, null); // For platforms which have a selection clipboard (such as X11), store the text there also clipboard = Toolkit.getDefaultToolkit().getSystemSelection(); if (clipboard != null) { clipboard.setContents(stringTransferable, null); } } /** * Completes the work necessary to do a user cut action. */ void doCutUserAction() { // Group as one action getUndoableEditSupport().beginUpdate(); getUndoableEditSupport().setEditName(GemCutter.getResourceString("UndoText_Cut")); DisplayedGem[] selectedDisplayedGems = getSelectedDisplayedGems(); // To cut, we simply copy and then delete, and group it all as one 'undo gesture' Clipboard clipboard = gemCutter.getClipboard(); DisplayedGemSelection displayedGemSelection = new DisplayedGemSelection(selectedDisplayedGems, gemCutter); clipboard.setContents(displayedGemSelection, displayedGemSelection); handleDeleteSelectedGemsGesture(); getUndoableEditSupport().endUpdate(); gemCutter.getPasteAction().setEnabled(true); } /** * Completes the paste user action * @param displayedGemSelection */ void doPasteUserAction(Transferable displayedGemSelection) { doPasteUserAction(displayedGemSelection, getTableTopPanel().getPasteLocation()); } /** * Completes the paste action. Takes the transferable specified and adds it to the tabletop * at the location specified in the parameter * @param displayedGemSelection * @param location */ void doPasteUserAction(Transferable displayedGemSelection, Point location) { Pair<DisplayedGem[], Set<Connection>> displayedGemsConnectionPair = null; DisplayedGem[] displayedGems = null; try { // Grab the data from the transferable displayedGemsConnectionPair = UnsafeCast.<Pair<DisplayedGem[], Set<Connection>>>unsafeCast(displayedGemSelection.getTransferData(DisplayedGemSelection.CONNECTION_PAIR_FLAVOR)); displayedGems = displayedGemsConnectionPair.fst(); if (location == null) { // If the location is null, we find the first available spot beneath the originally cut/copied gems Point preferredLocation = (Point)displayedGemSelection.getTransferData(DisplayedGemSelection.ORIGINAL_LOCATION_FLAVOR); location = findSpaceUnderneathFor(preferredLocation, displayedGems); } } catch (UnsupportedFlavorException e) { // Flavour not supported. return; } catch (IOException e) { // Data no longer available in the requested flavour. return; } Set<Connection> connectionSet = new LinkedHashSet<Connection>(displayedGemsConnectionPair.snd()); // Get the pre-state of argument target-related changes. StateEdit collectorArgumentStateEdit = new StateEdit(collectorArgumentStateEditable); // Get the categories of gem to add to the gem graph. Set<Gem> rootsToAdd = new HashSet<Gem>(); Set<DisplayedGem> displayedCollectorsToAdd = new HashSet<DisplayedGem>(); Set<DisplayedGem> displayedNonCollectorsToAdd = new HashSet<DisplayedGem>(); for (final DisplayedGem displayedGem : displayedGems) { Gem gem = displayedGem.getGem(); rootsToAdd.add(gem.getRootGem()); if (gem instanceof CollectorGem) { displayedCollectorsToAdd.add(displayedGem); // Also retarget untargeted collectors to the target gem. if (((CollectorGem)gem).getTargetCollectorGem() == null) { ((CollectorGem)gem).setTargetCollector(getTargetCollector()); } } else { displayedNonCollectorsToAdd.add(displayedGem); } } // Handle gems which can't be pasted. // Note: if this section is changed, canPasteToGemGraph() might also need to be updated. for (Iterator<DisplayedGem> it = displayedNonCollectorsToAdd.iterator(); it.hasNext(); ) { DisplayedGem displayedNonCollectorGem = it.next(); boolean removeGem = false; Gem gem = displayedNonCollectorGem.getGem(); if (gem instanceof ReflectorGem) { CollectorGem reflectorCollector = ((ReflectorGem)displayedNonCollectorGem.getGem()).getCollector(); // Don't paste this reflector gem if its collector will not be on the tabletop after the paste.. if (!(rootsToAdd.contains(reflectorCollector) || gemGraph.hasGem(reflectorCollector))) { removeGem = true; } } else if (gem instanceof FunctionalAgentGem) { // Can't paste if the entity has changed (as a result of recompilation). GemEntity pastingGemEntity = ((FunctionalAgentGem)gem).getGemEntity(); GemEntity expectedGemEntity = gemCutter.getPerspective().getVisibleGemEntity(pastingGemEntity.getName()); if (pastingGemEntity != expectedGemEntity) { removeGem = true; } } if (removeGem) { // Remove the gem. it.remove(); rootsToAdd.remove(gem); // ... remove from rootsToAdd in case the gem is a root. // Disconnect and remove connections. Connection outputConnection = gem.getOutputPart().getConnection(); if (outputConnection != null) { outputConnection.getSource().bindConnection(null); outputConnection.getDestination().bindConnection(null); connectionSet.remove(outputConnection); } for (int i = 0, nInputParts = gem.getNInputs(); i < nInputParts; i++) { PartInput inputPart = gem.getInputPart(i); Connection inputConnection = inputPart.getConnection(); if (inputConnection != null) { inputPart.bindConnection(null); inputConnection.getSource().bindConnection(null); connectionSet.remove(inputConnection); } } } } // After pruning the gems to add, we might not have anything left to do. if (rootsToAdd.isEmpty()) { return; } // Add the gems to the GemGraph gemGraph.addSubtrees(rootsToAdd); // Now notify the tabletop of the gem and connection additions. // We should post edits for collector additions first to avoid screwing up undo. List<DisplayedGem> orderedDisplayedGems = new ArrayList<DisplayedGem>(displayedCollectorsToAdd); orderedDisplayedGems.addAll(displayedNonCollectorsToAdd); for (final DisplayedGem displayedGem : orderedDisplayedGems) { // Update the location. Point oldLocation = displayedGem.getLocation(); Point newLocation = new Point(oldLocation.x + location.x, oldLocation.y + location.y); displayedGem.setLocation(newLocation); // Notify the tabletop. handleDisplayedGemAdded(displayedGem); } for (final Connection connection : connectionSet) { // Notify the tabletop handleConnectionAdded(connection); } // Handle selections.. // First clear the selection. selectDisplayedGem(null, false); // Keep track of the selection rectangle.. Rectangle selectedGemsBounds = null; // Now select all pasted gems. for (int i = 0; i < displayedGems.length; i++) { DisplayedGem ithDisplayedGem = displayedGems[i]; selectDisplayedGem(ithDisplayedGem, false); if (i == 0) { selectedGemsBounds = ithDisplayedGem.getBounds(); } else { selectedGemsBounds.add(ithDisplayedGem.getBounds()); } } // Ensure the gems are visible. if (selectedGemsBounds != null) { getTableTopPanel().scrollRectToVisible(selectedGemsBounds); } // Update the tabletop for the new gem graph state. updateForGemGraph(); // Post an edit for the paste operation. undoableEditSupport.postEdit(new UndoablePasteDisplayedGemsEdit(TableTop.this, orderedDisplayedGems, connectionSet, collectorArgumentStateEdit)); } /** * @param transferable the transferable to paste. * @return whether any of the current contents of the clipboard can be pasted on to the gem graph. * This returns true if the clipboard contains any gems which can be pasted. */ boolean canPasteToGemGraph(Transferable transferable) { if (transferable != null) { DisplayedGem[] displayedGems; try { // Grab the data from the transferable Pair <DisplayedGem[],Set<Connection>> displayedGemsConnectionPair = UnsafeCast.<Pair<DisplayedGem[],Set<Connection>>>unsafeCast(transferable.getTransferData(DisplayedGemSelection.CONNECTION_PAIR_FLAVOR)); displayedGems = displayedGemsConnectionPair.fst(); } catch (UnsupportedFlavorException e) { // Flavour not supported. return false; } catch (IOException e) { // Data no longer available in the requested flavour. return false; } // If the gems are all reflectors, and their collectors don't exist in the gem graph, false. // Otherwise true. for (final DisplayedGem dGem : displayedGems) { Gem ithGem = dGem.getGem(); if (ithGem instanceof ReflectorGem) { // We can paste this reflector if its collector is in the gem graph. if (getGemGraph().hasGem(((ReflectorGem)ithGem).getCollector())) { return true; } } else if (ithGem instanceof FunctionalAgentGem) { // We can paste this functional agent if its entity is the expected entity. GemEntity pastingGemEntity = ((FunctionalAgentGem)ithGem).getGemEntity(); GemEntity expectedGemEntity = gemCutter.getPerspective().getVisibleGemEntity(pastingGemEntity.getName()); if (pastingGemEntity == expectedGemEntity) { return true; } } else { return true; } } } return false; } /** * Do the work necessary to carry out a user-initiated action to add a gem. * It will be placed at the next available location as determined by the TableTop. * @param displayedGem DisplayedGem the gem to add */ void doAddGemUserAction(DisplayedGem displayedGem) { if (displayedGem != null) { // Need to make sure a collector has been assigned a name so that it can be properly positioned. if (displayedGem.getGem() instanceof CollectorGem) { CollectorGem cGem = (CollectorGem)displayedGem.getGem(); gemGraph.assignDefaultName(cGem); } doAddGemUserAction(displayedGem, findAvailableDisplayedGemLocation(displayedGem)); } } /** * Do the work necessary to carry out a user-initiated action to add a gem. * @param displayedGem DisplayedGem the gem to add * @param addPoint Point the location to add the gem */ void doAddGemUserAction(DisplayedGem displayedGem, Point addPoint){ addGemAndUpdate(displayedGem, addPoint); // Notify undo managers of the edit. undoableEditSupport.postEdit(new UndoableAddDisplayedGemEdit(TableTop.this, displayedGem)); } /** * Shrinks the tabletop to the maximum required by the gem boundaries */ void doShrinkTableTopUserAction() { // Gets the bounds of the current gems Rectangle gemGraphBounds = getGemGraphBounds(); Rectangle visibleRect = getTableTopPanel().getVisibleRect(); // Move everything to the top left corner, and then shrink int moveX = -gemGraphBounds.x; int moveY = -gemGraphBounds.y; visibleRect.x += moveX; visibleRect.y += moveY; // Aggregate changes undoableEditSupport.beginUpdate(); undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_FitTableTop")); moveAllGems(moveX, moveY); getTableTopPanel().setSize(gemGraphBounds.width, gemGraphBounds.height); getTableTopPanel().setPreferredSize(new Dimension(gemGraphBounds.width, gemGraphBounds.height)); // move from top left corner to top right corner. int xOffset = visibleRect.width < gemGraphBounds.width ? -10 : (visibleRect.width - gemGraphBounds.width) - 10; int yOffset = 10; moveAllGems(xOffset, yOffset); viewFocusedGem(); resizeForGems(); undoableEditSupport.endUpdateNoPost(); } /** * Moves the window to center on the 'focused' gem if possible * If not possible, then move to top left corner of tabletop */ private void viewFocusedGem() { DisplayedGem focusedGem = getFocusedDisplayedGem(); Rectangle visRect = getTableTopPanel().getVisibleRect(); int width = visRect.width; int height = visRect.height; int x; int y; if (focusedGem!= null) { x = focusedGem.getLocation().x - width / 2; y = focusedGem.getLocation().y - height / 2; if (x < 0) { x = 0; } if (y < 0) { y = 0; } } else { x = 0; y = 0; } Rectangle newVisRect = new Rectangle (x, y, width, height); getTableTopPanel().scrollRectToVisible(newVisRect); } /** * Begin logging every positional change on all the gems. (Should be used * in conjunction with endPositionUpdate(DisplayedGemLocationListener) * @return DisplayedGemLocationListener the listener that was added on.the gems */ private DisplayedGemLocationListener beginPositionUpdate() { // Create a listener to post gem translation edits DisplayedGemLocationListener locationListener = new DisplayedGemLocationListener() { public void gemLocationChanged(DisplayedGemLocationEvent e) { undoableEditSupport.postEdit(new UndoableChangeGemLocationEdit( TableTop.this, (DisplayedGem)e.getSource(), e.getOldBounds().getLocation())); } }; // Set the listener on all the displayed gems. for (final DisplayedGem displayedGem : getDisplayedGems()) { displayedGem.addLocationChangeListener(locationListener); } return locationListener; } /** * Remove the listener that was added on the gem * @param locationListener the listener to be removed */ private void endPositionUpdate(DisplayedGemLocationListener locationListener) { // Remove the listener from all the displayed gems. for (final DisplayedGem displayedGem : getDisplayedGems()) { displayedGem.removeLocationChangeListener(locationListener); } } /** * Do the work necessary to carry out a user-initiated action to arrange * the selected gems in the gem graph. * @param layoutArranger the object used to rearrange */ private void arrangeSelectionUserAction(Graph.LayoutArranger layoutArranger) { // Increment the update level for the edit undo. This will aggregate the gem translations. DisplayedGemLocationListener locationListener = beginPositionUpdate(); layoutArranger.arrangeGraph(); // The connections routes are invalid now. for (final DisplayedConnection displayedConnection : connectionDisplayMap.values()) { displayedConnection.purgeRoute(); } resizeForGems(); // Decrement the update level. This will post the edit if the level is zero. endPositionUpdate(locationListener); } /** * Do the work necessary to carry out a user-initiated action to arrange, align and space out * the gems on the gemGraph. This aligns all gems with the specified node * and ensures that no selected gems were overlapped. * @param layoutArranger * @param refNode (the node that the gems will be aligned to) */ void doTidyUserAction(Graph.LayoutArranger layoutArranger, Graph.Node refNode) { // Increment the update level for the edit undo. This will aggregate the gem translations. undoableEditSupport.beginUpdate(); DisplayedGemLocationListener locationListener = beginPositionUpdate(); undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_Tidy_Gems")); // Get the trees we're dealing with List<Tree> trees = layoutArranger.getTrees(); if (trees.isEmpty()) { return; } if (refNode == null) { refNode = trees.iterator().next().getRoot(); } Point referenceLocation = refNode.getLocation(); // Arrange the trees arrangeSelectionUserAction(layoutArranger); // Now we sort the trees from highest to lowest Graph.LayoutArranger.sortTreesOnYOrder(trees); // keep track of the difference between the coordinates int differenceInY = 0, differenceInX = 0; for (final Tree tree : trees) { // adjust the top right corner coordinate such that the refNode doesn't move Set<Graph.Node> nodes = new HashSet<Graph.Node>(); tree.getNodes(nodes); // If it contains the node, then we want to keep use the right bound of this tree as our reference point. if (nodes.contains(refNode)) { Point nodeLocation = refNode.getLocation(); int nodeYOffset = (nodeLocation.y - tree.getBounds().y); differenceInY += nodeYOffset; Rectangle rootBounds = tree.getRoot().getBounds(); differenceInX = rootBounds.x + rootBounds.width - nodeLocation.x; break; } differenceInY += tree.getBounds().height + 20; } // The upper right corner of the selected trees int highY = referenceLocation.y - differenceInY; int rightX = referenceLocation.x + differenceInX; for (final Tree tree : trees) { tree.setLocation(rightX - tree.getWidth(), highY); highY += tree.getHeight() + 20; } // Decrement the update level. This will post the edit if the level is zero. undoableEditSupport.endUpdate(); resizeForGems(); endPositionUpdate(locationListener); } /** * Tidy TableTop Function. This function aligns all the roots of the gem trees on the tabletop. * It is a composition of three user actions. Select All, Tidy Selection, Fit TableTop */ void doTidyTableTopAction() { // Increment the update level for the edit undo. This will aggregate the gem translations. undoableEditSupport.beginUpdate(); undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_TidyTableTop")); selectAllGems(); Graph.LayoutArranger layoutArranger = new Graph.LayoutArranger(getSelectedDisplayedGems()); doTidyUserAction(layoutArranger, null); doShrinkTableTopUserAction(); resizeForGems(); // Decrement the update level. This will post the edit if the level is zero. undoableEditSupport.endUpdate(); } /** * Do the work necessary to carry out a user-initiated action to change a gem's location. * @param displayedGem the gem to move * @param newLocation the new location of the gem. */ void doChangeGemLocationUserAction(DisplayedGem displayedGem, Point newLocation) { Rectangle oldBounds = displayedGem.getBounds(); // the location won't actually change if (oldBounds.getLocation().equals(newLocation)) { return; } // Change the gem location changeGemLocation(displayedGem, newLocation); // Notify undo managers of the edit. undoableEditSupport.postEdit(new UndoableChangeGemLocationEdit(TableTop.this, displayedGem, oldBounds.getLocation())); } /** * Do the work necessary to carry out a user-initiated action to make a connection. * Note: a connection may not necessarily made if it results in an invalid GemGraph. * @param srcPart the source part * @param destPart the destination part * @return Connection the connection which was made, or null if a connection could not be made. */ Connection doConnectIfValidUserAction(PartOutput srcPart, PartInput destPart) { // Return null if we can't connect. Connection connection = new Connection(srcPart, destPart); if (!gemGraph.canConnect(connection, getTypeCheckInfo())) { return null; } undoableEditSupport.beginUpdate(); // Get the emitter and collector argument state from before the disconnection. StateEdit collectorArgumentState = new StateEdit(collectorArgumentStateEditable); // Get value gem values before the connection, and connect. Map<ValueGem, ValueNode> valueGemToValueMap = getValueGemToValueMap(); connect(connection); // Re-type to update value gems. try { getGemGraph().typeGemGraph(getTypeCheckInfo()); } catch (TypeException e) { // Shouldn't happen. What now?? GemCutter.CLIENT_LOGGER.log(Level.SEVERE, "Error connecting gems."); e.printStackTrace(); } // Notify undo managers of any changes. postValueGemChanges(valueGemToValueMap); undoableEditSupport.postEdit(new UndoableConnectGemsEdit(TableTop.this, connection, collectorArgumentState)); undoableEditSupport.endUpdate(); // Update connectivity info for any connected code gems. if (srcPart.getGem() instanceof CodeGem) { updateForConnectivity(getCodeGemEditor((CodeGem)srcPart.getGem())); } if (destPart.getGem() instanceof CodeGem) { updateForConnectivity(getCodeGemEditor((CodeGem)destPart.getGem())); } return connection; } /** * Do the work necessary to carry out a user-initiated action to delete given gems from the TableTop. * Note: emitters for collectors in the set will also be deleted! * @param deleteSet the set of gems to delete */ private void doDeleteGemsUserAction(Set<Gem> deleteSet) { Set<Gem> gemsToDelete = new HashSet<Gem>(deleteSet); // make sure we don't delete the target gemsToDelete.remove(getTargetCollector()); if (gemsToDelete.isEmpty()) { return; } // make sure we stop intellicut getIntellicutManager().stopIntellicut(); // make sure popup menus go away if visible getTableTopPanel().hidePopup(); // collectors to delete - we must delete last to preserve invariant that emitters cannot exist without collectors Set<Gem> selectedCollectors = new HashSet<Gem>(gemsToDelete); selectedCollectors.retainAll(gemGraph.getCollectors()); // Make sure all associated emitters are also in the set of gems to delete. for (final Gem collectorGem : selectedCollectors) { gemsToDelete.addAll(((CollectorGem)collectorGem).getReflectors()); } // Increment the update level for the edit undo. This will aggregate edits (disconnections and deletions). undoableEditSupport.beginUpdate(); DisplayedGem firstDisplayedGem = getDisplayedGem((Gem)gemsToDelete.toArray()[0]); String editName = (gemsToDelete.size() != 1) ? GemCutterMessages.getString("UndoText_DeleteGems") : GemCutterMessages.getString("UndoText_Delete", firstDisplayedGem.getDisplayText()); undoableEditSupport.setEditName(editName); // now disconnect all gems, and delete all non-collector gems for (final Gem aGem : gemsToDelete) { // delete connections doDisconnectGemConnectionsUserAction(aGem); // remove if it's not a collector if (!(aGem instanceof CollectorGem)) { DisplayedGem displayedGem = getDisplayedGem(aGem); deleteGem(aGem); // Notify undo managers of the edit. undoableEditSupport.postEdit(new UndoableDeleteDisplayedGemEdit(TableTop.this, displayedGem)); } } // delete collectors last for (final Gem aGem : selectedCollectors) { DisplayedGem displayedGem = getDisplayedGem(aGem); deleteGem(aGem); // Notify undo managers of the edit. undoableEditSupport.postEdit(new UndoableDeleteDisplayedGemEdit(TableTop.this, displayedGem)); } // Decrement the update level. This will post the edit if the level is zero. undoableEditSupport.endUpdate(); } /** * Handle the gesture where the user attempts to disconnect two gem parts. * @param connection the connection to disconnect. */ void handleDisconnectGesture(Connection connection) { doDisconnectUserAction(connection); updateForGemGraph(); } /** * Do the work necessary to carry out a user-initiated action to disconnect a connection. * @param connection the connection to disconnect */ void doDisconnectUserAction(Connection connection) { // Increment the update level to aggregate any disconnection edits. undoableEditSupport.beginUpdate(); // Get the emitter and collector argument state from before the disconnection. StateEdit collectorArgumentState = new StateEdit(collectorArgumentStateEditable); // Now actually perform the disconnection disconnect(connection); // Notify undo managers of the disconnection. undoableEditSupport.postEdit(new UndoableDisconnectGemsEdit(TableTop.this, connection, collectorArgumentState)); // If the destination input is an unused code gem input, update its model, and post an edit for its state change. Gem destGem = connection.getDestination().getGem(); if (destGem instanceof CodeGem) { CodeGemEditor destCodeGemEditor = getCodeGemEditor((CodeGem)destGem); if (destCodeGemEditor.isUnusedArg(connection.getDestination()) || destGem.isBroken() && ((CodeGem)destGem).getCodeResultType() != null) { // check for incompatible connection. StateEdit stateEdit = new StateEdit(destCodeGemEditor, GemCutter.getResourceString("UndoText_DisconnectGems")); destCodeGemEditor.doSyntaxSmarts(); updateForConnectivity(destCodeGemEditor); stateEdit.end(); undoableEditSupport.postEdit(stateEdit); } } // Update code gem editors for connectivity. for (final CodeGemEditor codeGemEditor : codeGemEditorMap.values()) { updateForConnectivity(codeGemEditor); } // Decrement the update level. This will post the edit if the level is zero. undoableEditSupport.endUpdate(); } /** * Do the work necessary to carry out a user-initiated action to disconnect all of a gem's connections. * @param gemToDisconnect Gem the gem on which to operate */ private void doDisconnectGemConnectionsUserAction(Gem gemToDisconnect) { // Increment the update level to aggregate any disconnection edits. undoableEditSupport.beginUpdate(); List<PartConnectable> connectableParts = gemToDisconnect.getConnectableParts(); // disconnect any connectable parts which are actually connected for (final PartConnectable part : connectableParts) { if (part.isConnected()) { doDisconnectUserAction(part.getConnection()); } } // Decrement the update level. This will post the edit if the level is zero. undoableEditSupport.endUpdate(); } /** * Do the work necessary to carry out a user-initiated action to split a connection into a collector/emitter pair * @param connection the connection to be replaced */ void doSplitConnectionUserAction(Connection connection) { // The target of the new collector needs to be either #1 or #2 depending on which has the narrowest scope: // #1. the narrowest scoped targeted collector in its subtree.(the narrowest possible is the root collector for the tree) // For example: // private retargetExample list = // let // filterfunc value = value > Cal.Utilities.Summary.average list; // in // Cal.Collections.List.filter filterfunc list // ; // The value collector is targeted to filterfunc while list is targeted to retargetExample. // In order to split the connection between " > " and filterfunc, the new collector will need to target // filterfunc, the narrower of the 2 target collectors for value and list. // // #2. the narrowest target of any unbound, unburnt input of the descendant forest that is equal/wider than the root collector. // A very simple example: // private target x = // let // collGem x_1 = Cal.Core.Prelude.id (Cal.Core.Prelude.abs x_1) // in // collGem x // ; // Argument x_1 is targeted to collGem. Therefore to split the connection between abs and id gems, the new collector needs to // target collGem as well. In this case, collGem is also the root collector for the tree. final Gem srcGem = connection.getSource().getGem(); final Gem destGem = connection.getDestination().getGem(); CollectorGem targetForNewCollector = getTargetCollector(); // initially set as the tabletop's target collector int currentTargetDepth = GemGraph.getCollectorDepth(targetForNewCollector); // Should be 0 for widest scope // If destination gem is the target collector for the tabletop, skip the checks. if (!destGem.equals(targetForNewCollector)) { // #1. Find all the reflectors in the subtree and their corresponding collectors. // From the set, find the targeted collector with the deepest/narrowest scope. Set<ReflectorGem> emitterGemsInSubTree = UnsafeCast.<Set<ReflectorGem>>unsafeCast(GemGraph.obtainSubTreeGems(srcGem, ReflectorGem.class)); Set<CollectorGem> collectorGemsInSubTree = new HashSet<CollectorGem>(); if (!emitterGemsInSubTree.isEmpty()) { for (final ReflectorGem rGem : emitterGemsInSubTree) { collectorGemsInSubTree.add(rGem.getCollector()); } for (final CollectorGem cGem : collectorGemsInSubTree) { int cGemDepth = GemGraph.getCollectorDepth(cGem); int targetDepth = cGemDepth - 1; if (targetDepth > currentTargetDepth) { targetForNewCollector = cGem.getTargetCollectorGem(); currentTargetDepth = targetDepth; } } } // #2. Find the targeted collectors of all the unburnt, unbound inputs in the descendant forest. // Set that as the target for the new collector if the depth is in-between the previously found target collector in #1 and // the root gem of the tree. // The narrowest target that the new collector can retarget to is the root collector for the tree. This is so the new // collector would have the necessary visibility. Thus the rootCollectorDepth is also the maximum depth for the target. final CollectorGem rootCollector = destGem.getRootCollectorGem(); final int rootCollectorDepth; if (rootCollector == null) { rootCollectorDepth = 0; } else { rootCollectorDepth = GemGraph.getCollectorDepth(rootCollector); } // Skip the check if the current new collector's target is already at maximum depth if (rootCollectorDepth > currentTargetDepth) { List<PartInput> descendantInputs = GemGraph.obtainUnboundDescendantInputs(srcGem, TraversalScope.FOREST, InputCollectMode.UNBURNT_ONLY); for (final PartInput input : descendantInputs) { CollectorGem inputTarget = GemGraph.getInputArgumentTarget(input); final int targetDepth; if (inputTarget == null) { targetDepth = 0; } else { targetDepth = GemGraph.getCollectorDepth(inputTarget); } if (rootCollectorDepth >= targetDepth && targetDepth > currentTargetDepth) { targetForNewCollector = inputTarget; currentTargetDepth = targetDepth; } } } } undoableEditSupport.beginUpdate(); DisplayedGem newCollectorGem = createDisplayedCollectorGem(new Point(0, 0), targetForNewCollector); DisplayedGem newEmitterGem = createDisplayedReflectorGem(new Point(0, 0), (CollectorGem) newCollectorGem.getGem()); StateEdit collectorArgumentState = new StateEdit(collectorArgumentStateEditable); // Do the actual split action with the new gems splitConnectionWith(connection, newCollectorGem, newEmitterGem); // Display the edit box for the collector name for user input displayLetNameEditor((CollectorGem) newCollectorGem.getGem()); // Notify undo managers of the disconnection. getUndoableEditSupport().setEditName(GemCutter.getResourceString("UndoText_SplitConnection")); undoableEditSupport.postEdit(new UndoableSplitConnectionEdit(TableTop.this, connection, newCollectorGem, newEmitterGem, collectorArgumentState)); undoableEditSupport.endUpdate(); } /** * Clear the tabletop, and add an initial gem. */ void doNewTableTopUserAction() { // Increment the update level for the edit undo. This will aggregate the edits taken to clear the tabletop. undoableEditSupport.beginUpdate(); undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_ClearTableTop")); // Clean the tabletop. blankTableTop(); // Adds the initial collector gem. resetTargetForNewTableTop(); getTableTopPanel().repaint(); // Decrement the update level. This will post the edit if the level is zero. undoableEditSupport.endUpdate(); } /** * Select the subtree of the gems specified * @param gems */ List<DisplayedGem> doSelectSubtreeUser(Gem[] gems) { List<DisplayedGem> displayedGems = new ArrayList<DisplayedGem>(); for (final Gem gem : gems) { Set<Gem> subTree = GemGraph.obtainSubTree(gem); for (final Gem subTreeGem : subTree) { DisplayedGem displayedGem = getDisplayedGem(subTreeGem); displayedGems.add(displayedGem); selectDisplayedGem(displayedGem, false); } } return displayedGems; } /** * Handle the gesture where the user attempts to retarget an input to a different collector * @param inputArgument the argument to retarget. * @param targetableCollector the collector to which the argument will be retargeted. */ void handleRetargetInputArgumentGesture(Gem.PartInput inputArgument, CollectorGem targetableCollector) { handleRetargetInputArgumentGesture(inputArgument, targetableCollector, -1); } /** * Handle the gesture where the user attempts to retarget an input to a different collector * @param inputArgument the argument to retarget. * @param targetableCollector the collector to which the argument will be retargeted. * @param addIndex the index at which the retargeted argument will be placed, or -1 to add to the end. */ void handleRetargetInputArgumentGesture(Gem.PartInput inputArgument, CollectorGem targetableCollector, int addIndex) { // If retargeting an argument to a collector which encloses the root (or is the root), don't retarget the collector. // Otherwise, retarget the collector to the argument target. getUndoableEditSupport().beginUpdate(); // If not retargeting to an enclosing collector, retarget the root collector to the targetable collector. CollectorGem rootCollectorGem = inputArgument.getGem().getRootCollectorGem(); if (!targetableCollector.enclosesCollector(rootCollectorGem)) { doRetargetCollectorUserAction(rootCollectorGem, targetableCollector); } // Retarget the argument. doRetargetInputArgumentUserAction(inputArgument, targetableCollector, addIndex); getUndoableEditSupport().endUpdate(); } /** * Retarget an input argument from one collector to another. * @param inputArgument the input argument to retarget. * @param enclosingCollector the collector to which the input will be retargeted. * @param addIndex the index at which the retargeted argument will be placed, or -1 to add to the end. */ void doRetargetInputArgumentUserAction(Gem.PartInput inputArgument, CollectorGem enclosingCollector, int addIndex) { CollectorGem oldTarget = GemGraph.getInputArgumentTarget(inputArgument); // retarget the argument. int oldArgIndex = retargetInputArgument(inputArgument, enclosingCollector, addIndex); // Notify the undo manager. getUndoableEditSupport().postEdit(new UndoableRetargetInputArgumentEdit(this, inputArgument, oldTarget, enclosingCollector, oldArgIndex)); } /** * Retarget an input argument from one collector to another. * @param inputArgument the argument to retarget. * @param enclosingCollector the collector to which the input will be retargeted. * @param addIndex the index at which the retargeted argument will be placed, or -1 to add to the end. * @return the old argument index, or -1 if there was no old target. */ int retargetInputArgument(Gem.PartInput inputArgument, CollectorGem enclosingCollector, int addIndex) { // Defer to the gem graph method. int oldIndex = gemGraph.retargetInputArgument(inputArgument, enclosingCollector, addIndex); getTableTopPanel().repaint(getDisplayedPartConnectable(inputArgument).getBounds()); return oldIndex; } /** * Handle the gesture where the user attempts to retarget a collector to a different collector * @param collectorToRetarget the collector to retarget. * @param newTarget the collector to which the collector will be retargeted. */ void handleRetargetCollectorGesture(CollectorGem collectorToRetarget, CollectorGem newTarget) { // Perform the action. doRetargetCollectorUserAction(collectorToRetarget, newTarget); } /** * Retarget a collector from one collector to another. * @param collectorToRetarget the collector to retarget. * @param newTarget the collector to which the collector will be retargeted. */ void doRetargetCollectorUserAction(CollectorGem collectorToRetarget, CollectorGem newTarget) { CollectorGem oldTarget = collectorToRetarget.getTargetCollectorGem(); // retarget the collector. collectorToRetarget.setTargetCollector(newTarget); getTableTopPanel().repaint(getDisplayedGem(collectorToRetarget).getBounds()); // Notify the undo manager. getUndoableEditSupport().postEdit(new UndoableRetargetCollectorEdit(this, collectorToRetarget, oldTarget, newTarget)); } /** * Do an user-initiated add record field action * @param rcGem the gem to add a new field to */ void doAddRecordFieldUserAction (RecordCreationGem rcGem) { // Get the current state of the gem before the action Map<String, PartInput> prevStateMap = rcGem.getFieldNameToInputMap(); rcGem.addNewFields(1); updateForGemGraph(); getUndoableEditSupport().postEdit(new UndoableModifyRecordFieldEdit(rcGem, prevStateMap)); } /** * Do an user-initiated delete field action * @param rcGem the gem to delete a field from * @param fieldToDelete the field to be removed */ void doDeleteRecordFieldUserAction (RecordCreationGem rcGem, String fieldToDelete) { // Get the current state of the gem before the action Map<String, PartInput> prevStateMap = rcGem.getFieldNameToInputMap(); rcGem.deleteField(fieldToDelete); updateForGemGraph(); getUndoableEditSupport().postEdit(new UndoableModifyRecordFieldEdit(rcGem, prevStateMap)); } /** * Update the field in a record field selection gem and create an undoable edit * This will always mark the field as fixed. * @param gem * @param newFieldName the field name to set the gem to */ private void updateRecordFieldSelectionGem(RecordFieldSelectionGem gem, FieldName newFieldName) { String oldFieldName = gem.getFieldNameString(); boolean oldFixed = gem.isFieldFixed(); gem.setFieldName(newFieldName.getCalSourceForm()); gem.setFieldFixed(true); undoableEditSupport.postEdit(new UndoableChangeFieldSelectionEdit(this, gem, oldFieldName, oldFixed)); } /** * Handle the gesture where the user attempts to connect two gem parts. * A dialog may be shown if the attempted connection appears to be ok, but violates global type constraints. * @param srcPart the source part * @param destPart DisplayedPart the destination part * @return Connection the connection which was made, or null if a connection could not be made. */ Connection handleConnectGemPartsGesture(PartConnectable srcPart, PartInput destPart) { Connection newConnection = null; boolean canConnect = false; FieldName fieldName; undoableEditSupport.beginUpdate(); //this is a record field selection gem for which the field has been updated by the connection RecordFieldSelectionGem modifiedRecordFieldSelectionGem=null; ModuleTypeInfo currentModuleTypeInfo = getCurrentModuleTypeInfo(); //is connection possible as a normal connection or value gem connection canConnect = GemGraph.isCompositionConnectionValid(srcPart, destPart, currentModuleTypeInfo) || GemGraph.isDefaultableValueGemSource(srcPart, destPart, gemCutter.getConnectionContext()); //connection possible by modifying the record field selection field? if (!canConnect && ((fieldName=GemGraph.isValidConnectionToRecordFieldSelection(srcPart, destPart, currentModuleTypeInfo)) != null)) { modifiedRecordFieldSelectionGem=(RecordFieldSelectionGem)destPart.getGem(); updateRecordFieldSelectionGem(modifiedRecordFieldSelectionGem, fieldName); canConnect = true; } //burn connection possible by modifying the record field selection field? if (!canConnect && ((fieldName=GemGraph.isValidConnectionFromBurntRecordFieldSelection(srcPart, destPart, currentModuleTypeInfo)) !=null)) { modifiedRecordFieldSelectionGem = (RecordFieldSelectionGem)srcPart.getGem(); updateRecordFieldSelectionGem(modifiedRecordFieldSelectionGem, fieldName); canConnect = true; } if (canConnect) { // Make the connection newConnection = doConnectIfValidUserAction((PartOutput)srcPart, destPart); if (newConnection == null) { showCannotConnectDialog(); } } if (newConnection != null) { updateForGemGraph(); //if a record field selection gem was updated show the user the field selection editor if (modifiedRecordFieldSelectionGem != null) { final RecordFieldSelectionGem gem = modifiedRecordFieldSelectionGem; undoableEditSupport.beginUpdate(); //we must invoke the editor to run in the background so that its focus etc are not affected by the current connect operation SwingUtilities.invokeLater(new Runnable() { public void run() { final JComponent editor = displayRecordFieldSelectionEditor(gem); //we attach a listener to find out when the editor is finished in order to finalize the undoable edit operation editor.addFocusListener(new FocusListener() { public void focusGained(FocusEvent e) {} /** when the focus is lost it implies the editor is closing, any changes are committed so we can end the compound edit*/ public void focusLost(FocusEvent e) { undoableEditSupport.endUpdate(); editor.removeFocusListener(this); } }); } }); } } undoableEditSupport.endUpdate(); return newConnection; } /** * Show a dialog explaining why a connection can't be made.. */ void showCannotConnectDialog() { // the connection violates global type constraints. String title = GemCutter.getResourceString("CannotConnectDialogTitle"); String message = GemCutter.getResourceString("CannotConnectErrorMessage"); JOptionPane.showMessageDialog(TableTop.this.getTableTopPanel(), message, title, JOptionPane.ERROR_MESSAGE); } /** * Autoconnect using intellicut, if possible. * @param part the part which we should attempt to autoconnect using intellicut * @return boolean whether a connection was made */ boolean handleIntellicutAutoConnectGesture(DisplayedPartConnectable part) { return gemCutter.getIntellicutManager().attemptIntellicutAutoConnect(part); } /** * Start intellicut mode if appropriate * @param partClicked DisplayedPart the part in question * @return boolean whether the intellicut was started */ boolean maybeStartIntellicutMode(DisplayedPart partClicked){ if (!(partClicked instanceof DisplayedPartConnectable)) { return false; } // start intellicut mode if sink or source part, and not a burnt input PartConnectable part = ((DisplayedPartConnectable) partClicked).getPartConnectable(); if ((part instanceof PartInput && !((PartInput)part).isBurnt()) || (part instanceof PartOutput)) { if (!part.isConnected()) { // Special Case: We do not start Intellicut if the input part is out of range of the TableTop. if (((DisplayedPartConnectable) partClicked).getConnectionPoint().getY() <= 0) { return false; } // Sometimes (Eg: Broken Code gem), the TypeExpr is null, and should not have intellicut used on it. if (part.getType() == null) { return false; } // Proceed with Intellicut mode. getIntellicutManager().startIntellicutModeForTableTop((DisplayedPartConnectable) partClicked); return true; } } return false; } /** * Handle the gesture to delete a given gem from the TableTop. * A dialog may pop up asking for input, if collectors are selected but not their associated emitters. * @param gemToDelete Gem the gem to delete */ void handleDeleteGemGesture(Gem gemToDelete) { Set<Gem> gemsToDelete = new HashSet<Gem>(); gemsToDelete.add(gemToDelete); // defer to the more general method. handleDeleteGemsGesture(gemsToDelete); } /** * Handle the gesture to delete selected gems from the TableTop. * A dialog may pop up asking for input, if collectors are selected but not their associated emitters. */ void handleDeleteSelectedGemsGesture() { // defer to the more general method. Set<Gem> selectedGemsSet = new HashSet<Gem>(Arrays.asList(getSelectedGems())); handleDeleteGemsGesture(selectedGemsSet); } /** * returns the current manager that handles all the burning * @return TableTopBurnManager */ TableTopBurnManager getBurnManager() { return burnManager; } /** * Handle the gesture to delete gems from the TableTop. * A dialog may pop up asking for input, if collectors are selected but not their associated emitters. * @param deleteSet the set of gems to delete */ void handleDeleteGemsGesture(Set<Gem> deleteSet) { // make sure we don't delete the target deleteSet.remove(getTargetCollector()); // make sure we stop intellicut getIntellicutManager().stopIntellicut(); // make sure popup menus go away if visible getTableTopPanel().hidePopup(); Set<Gem> gemsToDelete = new HashSet<Gem>(deleteSet); // collectors to delete - we must delete last to preserve invariant that emitters cannot exist without collectors Set<Gem> selectedCollectors = new HashSet<Gem>(deleteSet); selectedCollectors.retainAll(gemGraph.getCollectors()); // Take appropriate action if we need to warn the user boolean deleteCollectorsWithEmitters = checkDeleteGemWarning(UnsafeCast.<Set<CollectorGem>>unsafeCast(selectedCollectors), gemsToDelete); if (!deleteCollectorsWithEmitters) { return; } // go through all the gems, adding all associated emitter and reflector gems to the set to delete for (final Gem collectorGem : selectedCollectors) { gemsToDelete.addAll(((CollectorGem)collectorGem).getReflectors()); } // Delete the gems. doDeleteGemsUserAction(gemsToDelete); // update gem graph updateForGemGraph(); } /** * Checks for the situation where a collector has been selected for deletion but one of its associated unselected * emitters is not selected and also connected to something. If so, display a dialog and prompt for the right * thing to do. * @param selectedCollectors the set of selected collectors * @param selectedGemsSet the set of selected gems * @return boolean whether the user wishes to continue with the deletion. */ private boolean checkDeleteGemWarning(Set<CollectorGem> selectedCollectors, Set<Gem> selectedGemsSet) { // Keep track of whether we warned the user that collector gems would be deleted boolean warnCollectorDelete = false; boolean deleteCollectorsWithEmitters = true; // first iterate through the set of collectors, and find out if there are any with associated // emitters or reflectors that are connected but not selected, so that we can warn the user if necessary for (Iterator<CollectorGem> selectedIt = selectedCollectors.iterator(); selectedIt.hasNext() && !warnCollectorDelete; ) { // Get the next element for consideration CollectorGem collectorGem = selectedIt.next(); // iterate through the associated emitters, checking if they are connected and not selected Set<ReflectorGem> reflectors = collectorGem.getReflectors(); for (final ReflectorGem reflectorGem : reflectors) { if (reflectorGem.isConnected() && !(selectedGemsSet.contains(reflectorGem))) { warnCollectorDelete = true; break; } } } // if necessary, warn the user if there are associated emitters which are connected, as they will be deleted if (warnCollectorDelete) { // Formulate the warning. String titleString = GemCutter.getResourceString("WarningDialogTitle"); String message = GemCutter.getResourceString("DeleteCollectorWarning"); // Show the warning. See if the user really wants to delete collector and emitters int option = JOptionPane.showConfirmDialog(getTableTopPanel(), message, titleString, JOptionPane.YES_NO_OPTION, javax.swing.JOptionPane.WARNING_MESSAGE); // if the user answered no, don't delete collectors with emitters if (option == JOptionPane.NO_OPTION) { deleteCollectorsWithEmitters = false; } } return deleteCollectorsWithEmitters; } /* * Methods supporting XMLPersistable ******************************************** */ /** * Attach the saved form of this object as a child XML node. * @param parentNode Node the node that will be the parent of the generated XML. The generated XML will * be appended as a subtree of this node. * Note: parentNode must be a node type that can accept children (eg. an Element or a DocumentFragment) */ public void saveXML(Node parentNode) { Document document = (parentNode instanceof Document) ? (Document)parentNode : parentNode.getOwnerDocument(); // Create the TableTop element Element resultElement = document.createElement(GemPersistenceConstants.TABLETOP_TAG); parentNode.appendChild(resultElement); // Create a new gem context. GemContext gemContext = new GemContext(); // Create a document fragment to hold the gem elements DocumentFragment documentFragment = document.createDocumentFragment(); // Get the gem graph gems, separate these into collectors and other gems. Set<CollectorGem> collectorSet = gemGraph.getCollectors(); Set<Gem> otherGems = new HashSet<Gem>(gemGraph.getGems()); otherGems.removeAll(collectorSet); // Organize the collector gems so that outer collectors come before inner ones. Set<CollectorGem> orderedCollectorSet = GemGraph.getOrderedCollectorSet(collectorSet); // Attach gem elements to the doc fragment - collectors first, then other gems. for (final CollectorGem collectorGem : orderedCollectorSet) { getDisplayedGem(collectorGem).saveXML(documentFragment, gemContext); } for (final Gem otherGem : otherGems) { getDisplayedGem(otherGem).saveXML(documentFragment, gemContext); } // Now attach the gem elements resultElement.appendChild(documentFragment); // Attach connection elements Collection<DisplayedConnection> displayedConnections = connectionDisplayMap.values(); for (final DisplayedConnection displayedConnection : displayedConnections) { displayedConnection.getConnection().saveXML(resultElement, gemContext); } // Now add Tabletop-specific info // For now, there isn't any. Maybe you could save some metadata here. Trivial stuff like type colours? } /** * Return whether the element given corresponds to a displayed gem. * @param node * @return boolean */ private boolean isDisplayedGemElement(Node node) { return GemCutterPersistenceHelper.isDisplayedGemElement(node); } /** * Load this object's state. * @param element the element at the head of the subtree which represents the object to reconstruct. * @param perspective the perspective into which the load should occur. * @param loadStatus the ongoing status of the load. */ public void loadXML(Element element, Perspective perspective, Status loadStatus) { try { XMLPersistenceHelper.checkTag(element, GemPersistenceConstants.TABLETOP_TAG); } catch (BadXMLDocumentException bxde) { loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("SOM_TableTopLoadFailure"), bxde)); } // Make sure the tabletop is clear before we continue further. blankTableTop(); // Temporarily set the target's name to something invalid, to avoid name collisions with any gems being loaded. gemGraph.getTargetCollector().setName("!tempTargetLoadName!"); // Also disable automatic gem graph reflector updating. gemGraph.setArgumentUpdatingDisabled(true); // The set of loaded collector gems. Set<Gem> collectorGemSet = new HashSet<Gem>(); // We instantiate emitters last, as they cannot be instantiated without their associated collectors. Set<Element> emitterElements = new HashSet<Element>(); // A new context in which the gems are loaded. GemContext gemContext = new GemContext(); Argument.LoadInfo loadInfo = new Argument.LoadInfo(); // Set of IDs of gem elements which couldn't be instantiated Set<String> unknownGemIds = new HashSet<String>(); try { // Instantiate displayed gem children, except for emitters (which need collectors to be instantiated first) boolean targetWasLoaded = false; Node childNode; for (childNode = element.getFirstChild(); isDisplayedGemElement(childNode); childNode = childNode.getNextSibling()) { Element displayedGemElement = (Element)childNode; // Get the gem node. Node gemNode = displayedGemElement.getFirstChild(); try { XMLPersistenceHelper.checkIsElement(gemNode); } catch (BadXMLDocumentException e) { loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("SOM_ErrorLoadingFromNode") + gemNode.getLocalName(), e)); } // instantiate gem node Class<?> gemClass = getGemClass((Element)gemNode); if (gemClass == null) { // unrecognized type String gemId = Gem.getGemId(displayedGemElement); if (gemId != null) { unknownGemIds.add(gemId); } loadStatus.add(new Status(Status.Severity.WARNING, GemCutter.getResourceString("SOM_UnrecognizedGemType") + displayedGemElement.getLocalName(), null)); } else if (gemClass == ReflectorGem.class) { // emitter gem - save for later emitterElements.add(displayedGemElement); } else { // a gem which we should instantiate now. Do this if possible. DisplayedGem displayedGem = getDisplayedGem(displayedGemElement, gemContext, loadInfo, perspective, loadStatus); if (displayedGem == null) { // Couldn't instantiate. String gemId = Gem.getGemId(displayedGemElement); if (gemId != null) { unknownGemIds.add(gemId); } continue; } Gem gem = displayedGem.getGem(); if (!targetWasLoaded && gem instanceof CollectorGem) { // HACK(?): reassign things so the gem graph target looks like the loaded target. CollectorGem newTarget = (CollectorGem)gem; CollectorGem gemGraphTarget = gemGraph.getTargetCollector(); gemGraphTarget.setName(newTarget.getUnqualifiedName()); targetDisplayedCollector.setLocation(displayedGem.getLocation()); gemContext.addGem(gemGraphTarget, gemContext.getIdentifier(newTarget, false)); loadInfo.remapGem(newTarget, gemGraphTarget); targetWasLoaded = true; } else { // Add the gem to the tabletop addGem(displayedGem, displayedGem.getLocation()); } // Add to the collector map if it's a collector if (gem instanceof CollectorGem) { collectorGemSet.add(gem); } } } if (!targetWasLoaded) { loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("SOM_TargetDidntLoad"), null)); gemGraph.getTargetCollector().setName("target"); // Just set to something valid.. } // Instantiate displayed emitters. for (final Element emitterElement : emitterElements) { DisplayedGem displayedGem = getDisplayedGem(emitterElement, gemContext, loadInfo, perspective, loadStatus); if (displayedGem == null) { // Couldn't instantiate. String gemId = Gem.getGemId(emitterElement); if (gemId != null) { unknownGemIds.add(gemId); } continue; } // Add the gem to the tabletop addGem(displayedGem, displayedGem.getLocation()); } // update collector and reflector argument info. Set<CollectorGem> collectorSet = gemGraph.getCollectors(); for (final CollectorGem collectorGem : collectorSet) { try { collectorGem.loadArguments(loadInfo, gemContext); } catch (BadXMLDocumentException bdxe) { loadStatus.add(new Status(Status.Severity.WARNING, GemCutter.getResourceString("SOM_ErrorLoadingCollector"), bdxe)); } } for (final Gem gem : gemGraph.getGems()) { if (gem instanceof ReflectorGem) { try { ((ReflectorGem)gem).loadArguments(loadInfo, gemContext); } catch (BadXMLDocumentException bdxe) { loadStatus.add(new Status(Status.Severity.WARNING, GemCutter.getResourceString("SOM_ErrorLoadingEmitter"), bdxe)); } } } // Instantiate connections. while (childNode != null && childNode instanceof Element && Connection.isConnectionElement((Element)childNode)) { Element connectionElement = (Element)childNode; // Make the connection. Skip connecting unknown gems. Connection connection = null; try { connection = Connection.elementToConnection(connectionElement, gemContext, unknownGemIds); } catch (BadXMLDocumentException bxde) { loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("SOM_CouldntMakeConnection"), bxde)); } if (connection != null) { Gem.PartOutput fromPart = connection.getSource(); Gem.PartInput toPart = connection.getDestination(); if (fromPart == null || toPart == null) { loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("CouldntMakeConnection"), null)); } else { // Attempt the connection try { connect(connection); } catch (Exception e) { loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("CouldntMakeConnection"), e)); } } } // get the next element childNode = childNode.getNextSibling(); } // Make sure the inputs targets actually make sense. gemGraph.validateInputTargets(); // Update reflectors. for (final CollectorGem gem : collectorSet) { gem.updateReflectedInputs(); } // Now that we have instantiated as much of the gem graph as we can, // carry out some validation to ensure that the current gem graph is valid. boolean gemGraphValid = true; try { gemGraph.typeGemGraph(getTypeCheckInfo()); } catch (TypeException te) { gemGraphValid = false; loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("SOM_InvalidGemGraph"), null)); } // If the gem graph is not valid, disconnect value gems, since they are a big culprit. if (!gemGraphValid) { // The biggest culprit is value gems. Disconnect them all. for (final Connection connection : gemGraph.getConnections()) { if (connection.getSource().getGem() instanceof ValueGem) { disconnect(connection); } } // Make sure the inputs targets actually make sense. gemGraph.validateInputTargets(); // Update reflectors. for (final CollectorGem gem : collectorSet) { gem.updateReflectedInputs(); } // try again.. gemGraphValid = true; try { gemGraph.typeGemGraph(getTypeCheckInfo()); } catch (TypeException te) { gemGraphValid = false; } } // If the gem graph is still not valid, disconnect the remaining connections. // This should almost always work. if (!gemGraphValid) { // Disconnect remaining connections. for (final Connection connection : gemGraph.getConnections()) { disconnect(connection); } // Make sure the inputs targets actually make sense. gemGraph.validateInputTargets(); // Update reflectors. for (final CollectorGem gem : collectorSet) { gem.updateReflectedInputs(); } try { // Type check.. gemGraph.typeGemGraph(getTypeCheckInfo()); } catch (TypeException e) { GemCutter.CLIENT_LOGGER.log(Level.SEVERE, GemCutter.getResourceString("SOM_CantConstructLoadState")); doNewTableTopUserAction(); } } // sanity test.. if (GemCutterPersistenceHelper.isDisplayedGemElement(childNode)) { BadXMLDocumentException bxde = new BadXMLDocumentException(childNode, "Gem elements must appear before connection elements."); loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("SOM_InvalidXMLstructure"), bxde)); } } finally { // Try to recover from unexpected exceptions.. // Now make sure the tabletop is actually big enough to hold all the gems. resizeForGems(); // Re-enable automatic gem graph reflector updating. gemGraph.setArgumentUpdatingDisabled(false); // Activate codegem smarts. updateCodeGemEditors(); // Say that all burned inputs are manually burned. Set<Gem> gemSet = gemGraph.getRoots(); for (final Gem rootGem : gemSet) { List<PartInput> burntInputs = GemGraph.obtainUnboundDescendantInputs(rootGem, GemGraph.TraversalScope.TREE, GemGraph.InputCollectMode.BURNT_ONLY); burnManager.getManuallyBurntSet().addAll(burntInputs); } } } /** * Get the identifier for the first gem element (by preorder traversal) descending from a given element. * @param gemAncestorElement the ancestor element of the gem to id. * @return the gem's identifier, or null if a gem descendant cannot be found with an appropriate attribute. */ public static String getGemId(Element gemAncestorElement) { // Get the descendant nodes with the gem tag. NodeList nodeList = gemAncestorElement.getElementsByTagName(GemPersistenceConstants.GEM_TAG); // Iterate over them, looking for an element with the id attribute. int nNodes = nodeList.getLength(); for (int i = 0; i < nNodes; i++) { Node node = nodeList.item(i); if (node instanceof Element) { String gemId = ((Element)node).getAttribute(GemPersistenceConstants.GEM_ID_ATTR); if (!gemId.equals("")) { return gemId; } } } return null; } /** * Get the class of gem represented by the given element. * @param gemElement Element the element representing the gem in question. * @return Class the class represented by the element, or null if unknown. */ private static Class<? extends Gem> getGemClass(Element gemElement) { String tagName = gemElement.getLocalName(); if (tagName.equals(GemPersistenceConstants.CODE_GEM_TAG)) { return CodeGem.class; } else if (tagName.equals(GemPersistenceConstants.COLLECTOR_GEM_TAG)) { return CollectorGem.class; } else if (tagName.equals(GemPersistenceConstants.EMITTER_GEM_TAG)) { return ReflectorGem.class; } else if (tagName.equals(GemPersistenceConstants.RECORD_FIELD_SELECTION_GEM_TAG)) { return RecordFieldSelectionGem.class; } else if (tagName.equals(GemPersistenceConstants.FUNCTIONAL_AGENT_GEM_TAG)) { return FunctionalAgentGem.class; } else if (tagName.equals(GemPersistenceConstants.VALUE_GEM_TAG)) { return ValueGem.class; } else if (tagName.equals(GemPersistenceConstants.RECORD_CREATION_GEM_TAG)) { return RecordCreationGem.class; } else { // Unknown gem type return null; } } /** * Attach the saved form of the given displayed gem as a child XML node. * @param parentNode Node the node that will be the parent of the generated XML. * The generated XML will be appended as a subtree of this node. * @param displayedGemToSave the displayed gem to save. * Note: parentNode must be a node type that can accept children (eg. an Element or a DocumentFragment) * @param gemContext the context in which the gem is saved. */ public void saveDisplayedGem(DisplayedGem displayedGemToSave, Node parentNode, GemContext gemContext) { Document document = (parentNode instanceof Document) ? (Document)parentNode : parentNode.getOwnerDocument(); // Create the displayed gem element Element resultElement = document.createElement(GemPersistenceConstants.DISPLAYED_GEM_TAG); parentNode.appendChild(resultElement); // Add an element for the info for the underlying gem. displayedGemToSave.getGem().saveXML(resultElement, gemContext); // Add a child location element Element locationElement = GemCutterPersistenceHelper.pointToElement(displayedGemToSave.getLocation(), document); resultElement.appendChild(locationElement); } /** * Get the displayed gem represented by the given element. * @param displayedGemElement Element the element representing the displayed gem to instantiate. * @param gemContext the context in which the gem is being instantiated. * @param loadInfo the argument info for this load session. * @param perspective the perspective in which the load is taking place. * @param loadStatus the ongoing status of the load. * @return DisplayedGem the displayed gem represented by the given element. Null if the gem couldn't be instantiated. */ private DisplayedGem getDisplayedGem(Element displayedGemElement, GemContext gemContext, Argument.LoadInfo loadInfo, Perspective perspective, Status loadStatus) { try { XMLPersistenceHelper.checkTag(displayedGemElement, GemPersistenceConstants.DISPLAYED_GEM_TAG); // Get the gem node. Node gemNode = displayedGemElement.getFirstChild(); XMLPersistenceHelper.checkIsElement(gemNode); Element gemElement = (Element)gemNode; // Determine the location Node locationNode = gemNode.getNextSibling(); XMLPersistenceHelper.checkIsElement(locationNode); Point location; try { location = GemCutterPersistenceHelper.elementToPoint((Element)locationNode); } catch (BadXMLDocumentException bxde) { loadStatus.add(new Status(Status.Severity.WARNING, GemCutter.getResourceString("SOM_CantLoadLocation"), bxde)); location = new Point(); // default location } Class<? extends Gem> gemClass = getGemClass(gemElement); Gem loadedGem; if (gemClass == CodeGem.class) { loadedGem = CodeGem.getFromXML(gemElement, gemContext, gemCutter.getCodeGemAnalyser()); } else if (gemClass == CollectorGem.class) { loadedGem = CollectorGem.getFromXML(gemElement, gemContext, loadInfo); } else if (gemClass == RecordFieldSelectionGem.class) { loadedGem = RecordFieldSelectionGem.getFromXML(gemElement, gemContext, loadInfo); } else if (gemClass == FunctionalAgentGem.class) { loadedGem = FunctionalAgentGem.getFromXML(gemElement, gemContext, gemCutter.getWorkspace()); } else if (gemClass == ReflectorGem.class) { loadedGem = ReflectorGem.getFromXML(gemElement, gemContext, loadInfo); } else if (gemClass == ValueGem.class) { loadedGem = ValueGem.getFromXML(gemElement, gemContext, gemCutter.getValueRunner(), perspective.getWorkingModuleName()); } else if (gemClass == RecordCreationGem.class) { loadedGem = RecordCreationGem.getFromXML(gemElement, gemContext, loadInfo); } else { throw new BadXMLDocumentException(gemElement, "Unhandled gem class: " + gemClass); } return createDisplayedGem(loadedGem, location); } catch (BadXMLDocumentException bxde) { loadStatus.add(new Status(Status.Severity.ERROR, GemCutter.getResourceString("SOM_CantInstantiateGem"), bxde)); } // Not one of the types we know about. return null; } /** * Adds a gem graph listener that will listen to macroscopic changes such as connections and additions of gems * @param gemGraphChangeListener the listener to add */ void addGemGraphChangeListener(GemGraphChangeListener gemGraphChangeListener) { gemGraph.addGraphChangeListener(gemGraphChangeListener); } }