/* * 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. */ /* * ArgumentTree.java * Creation date: May 25, 2004. * By: Edward Lam */ package org.openquark.gems.client; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; import java.awt.Rectangle; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceAdapter; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DropTargetDragEvent; import java.awt.event.MouseEvent; import java.io.IOException; import java.util.Arrays; import java.util.Enumeration; import java.util.EventObject; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JTree; import javax.swing.ToolTipManager; import javax.swing.event.CellEditorListener; import javax.swing.event.ChangeEvent; import javax.swing.event.EventListenerList; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeWillExpandListener; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.ExpandVetoException; import javax.swing.tree.TreeCellEditor; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.openquark.cal.metadata.FunctionMetadata; import org.openquark.gems.client.ArgumentTreeNode.ArgumentNode; import org.openquark.gems.client.Gem.PartInput; import org.openquark.gems.client.valueentry.StructuredValueEditor; import org.openquark.gems.client.valueentry.ValueEditor; import org.openquark.gems.client.valueentry.ValueEditorEvent; import org.openquark.gems.client.valueentry.ValueEditorHierarchyManager; import org.openquark.gems.client.valueentry.ValueEditorListener; import org.openquark.gems.client.valueentry.ValueEntryPanel; import org.openquark.gems.client.valueentry.ValueEntryPanel.MirrorValueEntryPanel; import org.openquark.util.UnsafeCast; import org.openquark.util.ui.HighlightTree; import org.openquark.util.ui.HighlightTreeDnDHandler; /** * This class represents the JTree component that is used in the ArgumentExplorer. * @author Edward Lam */ public class ArgumentTree extends HighlightTree { private static final long serialVersionUID = 5069177050123093096L; /** The completely-transparent color. */ private static final Color TRANSPARENT_COLOR = new Color(0, 0, 0, 0); /** The TableTopExplorerOwner of the TableTopExplorer using this tree. */ private final ArgumentExplorerOwner owner; /** Whether or not mouse input for the tree is enabled. */ private boolean mouseInputEnabled = true; /** The cell renderer. */ private final CellRenderer cellRenderer = new CellRenderer(this); /** The dnd handler for this tree. */ private final HighlightTreeDnDHandler dndHandler = new DnDHandler(); /** The node currently being dragged as a transferable. */ private ArgumentTreeNode draggingNode = null; /** * The custom cell renderer for the ArgumentExplorer tree. * @author Edward Lam */ public static class CellRenderer extends DefaultTreeCellRenderer { private static final long serialVersionUID = 2722279109617670004L; /* the various icons used in the tree */ private static final ImageIcon emitterIcon; private static final ImageIcon reflectorIcon; private static final ImageIcon targetEmitterIcon; private static final ImageIcon targetReflectorIcon; private static final ImageIcon partInputIcon; private static final ImageIcon unusedInputIcon; static { // Make icon objects emitterIcon = new ImageIcon(Object.class.getResource("/Resources/emitter.gif")); reflectorIcon = new ImageIcon(Object.class.getResource("/Resources/reflector.gif")); targetEmitterIcon = new ImageIcon(Object.class.getResource("/Resources/targetEmitter.gif")); targetReflectorIcon = new ImageIcon(Object.class.getResource("/Resources/targetReflector.gif")); partInputIcon = new ImageIcon(Object.class.getResource("/Resources/partinput.gif")); unusedInputIcon = new ImageIcon(Object.class.getResource("/Resources/unusedInput.gif")); } /** The argument tree for which rendering takes place. */ private final ArgumentTree argumentTree; /** The default font. */ private final Font defaultFont = getFont(); /** * A map from input to an editor containing a value for that input. * If non-null, input values are displayed. */ private Map<PartInput, ValueEditor> inputToArgumentPanelMap = null; /** * @param argumentTree the tree being rendered. */ public CellRenderer(ArgumentTree argumentTree) { this.argumentTree = argumentTree; } /** * Get the appropriate small icon to use for the given collector gem. * @param collectorGem the collector gem to be represented. * @return the corresponding icon. */ public static ImageIcon getCollectorIcon(CollectorGem collectorGem) { boolean isTarget = collectorGem.getTargetCollector() == null; return collectorGem.getReflectedInputs().isEmpty() ? (isTarget ? targetEmitterIcon : emitterIcon) : (isTarget ? targetReflectorIcon : reflectorIcon); } /** * Get the appropriate small icon to use for the given argument node. * @param argumentNode the node for the argument to be represented. * @return the corresponding icon. */ public static ImageIcon getInputIcon(ArgumentTreeNode.ArgumentNode argumentNode) { Gem.PartInput inputPart = argumentNode.getArgument(); return ((ArgumentTreeNode.CollectorNode)argumentNode.getParent()).getCollectorGem().isReflected(inputPart) ? partInputIcon : unusedInputIcon; } /** * {@inheritDoc} */ @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { ImageIcon customIcon = null; String customText = value.toString(); if (value instanceof ArgumentTreeNode.CollectorNode) { CollectorGem collectorGem = ((ArgumentTreeNode.CollectorNode)value).getCollectorGem(); // Show an emitter rather than a collector. customIcon = getCollectorIcon(collectorGem); customText = collectorGem.getUnqualifiedName(); } else if (value instanceof ArgumentTreeNode.ArgumentNode) { ArgumentTreeNode.ArgumentNode argumentNode = (ArgumentTreeNode.ArgumentNode)value; Gem.PartInput inputPart = argumentNode.getArgument(); // Get the input icon. customIcon = getInputIcon(argumentNode); customText = inputPart.getNameString(); // If we're in run mode, show input values if available. if (inputToArgumentPanelMap != null) { ValueEditor inputEditor = inputToArgumentPanelMap.get(inputPart); if (inputEditor != null) { customText += ": " + inputEditor.getValueNode().getTextValue(); } } } else { // Unrecognized node type. What to do? // For now just go with the defaults. } // If we're in run mode, change the font. if (inputToArgumentPanelMap != null) { setFont(new Font("Dialog", Font.BOLD, ValueEntryPanel.PANEL_HEIGHT * 2 / 3)); } else { setFont(defaultFont); } // Always render a dragging node as selected sel |= (value == argumentTree.getDraggingNode()); super.getTreeCellRendererComponent(tree, customText, sel, expanded, leaf, row, hasFocus); if (customIcon != null) { setIcon(customIcon); setDisabledIcon(customIcon); } return this; } /** * Show or hide argument values. * @param inputToArgumentPanelMap a map from input to a value entry panel editing its argument value. * If non-null, argument values will be shown. If null, argument values will not be shown. */ public void showArgumentValues(Map<PartInput, ValueEditor> inputToArgumentPanelMap) { this.inputToArgumentPanelMap = (inputToArgumentPanelMap == null) ? null : new HashMap<PartInput, ValueEditor>(inputToArgumentPanelMap); } } /** * This is a value editor which encapsulates another value editor, and displays a component to the left of it. * @author Edward Lam */ private static class AugmentedValueEditor extends StructuredValueEditor { private static final long serialVersionUID = -4156318472919110794L; /** The value editor within this value editor. */ private final ValueEditor simpleValueEditor; /** * Constructor for an augmented value editor. * @param hierarchyManager * @param leftComponent the component which will appear on the left of the value editor. * @param simpleValueEditor the editor encapsulated by this editor. */ AugmentedValueEditor(ValueEditorHierarchyManager hierarchyManager, JComponent leftComponent, ValueEditor simpleValueEditor) { super(hierarchyManager); this.simpleValueEditor = simpleValueEditor; setOpaque(false); setBackground(TRANSPARENT_COLOR); setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setBorder(BorderFactory.createEmptyBorder()); add(leftComponent); add(simpleValueEditor); } /** * {@inheritDoc} */ @Override public Component getDefaultFocusComponent() { return simpleValueEditor.getDefaultFocusComponent(); } /** * {@inheritDoc} */ @Override public void setInitialValue() { simpleValueEditor.setInitialValue(); } /** * {@inheritDoc} */ @Override protected void handleElementLaunchingEditor() { } } /** * The custom cell editor for the ArgumentExplorer tree. * @author Edward Lam */ private class CellEditor implements TreeCellEditor { /** A list of event listeners for this editor. */ private final EventListenerList listenerList = new EventListenerList(); /** A change event instance. */ private final ChangeEvent changeEvent = new ChangeEvent(this); /** The hierarchy manager for the display of value editors in run mode. */ private ValueEditorHierarchyManager hierarchyManager = null; /** The editor currently displayed, if an editor is currently displayed. */ private ValueEditor valueEditor = null; /** a map from input to an editor containing a value for that input. */ private final Map<PartInput, ValueEditor> inputToArgumentPanelMap; /** * Constructor for the cell editor. * @param inputToArgumentPanelMap a map from input to a value entry panel editing its argument value. */ CellEditor(Map<PartInput, ValueEditor> inputToArgumentPanelMap) { this.inputToArgumentPanelMap = new HashMap<PartInput, ValueEditor>(inputToArgumentPanelMap); } /** * Get the value editor to being used to display the edit value. * @param inputPart the input for which the editor will be returned. * @return ValueEditor the editor used to display the value. */ private ValueEntryPanel getArgumentPanel(PartInput inputPart) { return (ValueEntryPanel)inputToArgumentPanelMap.get(inputPart); } /** * {@inheritDoc} */ public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) { // Just render the input dot. Otherwise, render the whole cell. JComponent cellRendererComponent = new JLabel(CellRenderer.getInputIcon((ArgumentNode)value)); // Set up the value editor stuff. ValueEditor mirroredValueEditor = getMirroredEntryPanel((ArgumentTreeNode)value); valueEditor = new AugmentedValueEditor(getValueEditorHierarchyManager(), cellRendererComponent, mirroredValueEditor); getValueEditorHierarchyManager().addTopValueEditor(valueEditor); getValueEditorHierarchyManager().addEditorToHierarchy(mirroredValueEditor, valueEditor); // Make the panel as wide as the visible part of the tree. Dimension preferredSize = valueEditor.getPreferredSize(); preferredSize.width = tree.getVisibleRect().width - tree.getPathBounds(new TreePath(((ArgumentTreeNode)value).getPath())).x; valueEditor.setPreferredSize(preferredSize); valueEditor.revalidate(); return valueEditor; } /** * @return the hierarchy manager used to instantiate value editors.. */ private ValueEditorHierarchyManager getValueEditorHierarchyManager() { // Lazily create a hierarchy manager if none yet exists. if (hierarchyManager == null) { hierarchyManager = new ValueEditorHierarchyManager(owner.getValueEditorManager()); // Set up the hierarchy so that hierarchy commit/cancel events close the top-level editor. hierarchyManager.setHierarchyCommitCancelHandler(new ValueEditorHierarchyManager.HierarchyCommitCancelHandler() { public void handleHierarchyCommitCancel(boolean commit) { if (commit) { stopEditing(); } else { cancelEditing(); } } }); } return hierarchyManager; } /** * Get a mirrored entry panel to edit the value for the given argument tree node. * @param argumentTreeNode * @return a mirrored entry panel that can be used by the argument tree to edit the argument's value. */ private MirrorValueEntryPanel getMirroredEntryPanel(ArgumentTreeNode argumentTreeNode) { if (argumentTreeNode instanceof ArgumentTreeNode.ArgumentNode) { ArgumentTreeNode.ArgumentNode argumentNode = (ArgumentTreeNode.ArgumentNode)argumentTreeNode; Gem.PartInput inputPart = argumentNode.getArgument(); MirrorValueEntryPanel mirroredEntryPanel = new ValueEntryPanel.MirrorValueEntryPanel(getValueEditorHierarchyManager(), getArgumentPanel(inputPart)); // Add a listener to update the node when the value changes.. mirroredEntryPanel.addValueEditorListener(new ValueEditorListener() { public void valueChanged(ValueEditorEvent evt) { } public void valueCommitted(ValueEditorEvent evt) { // On commit, generate nodeChanged() events for every node. getArgumentTreeModel().targetArgumentNodesChanged(); } public void valueCanceled(ValueEditorEvent evt) { } }); return mirroredEntryPanel; } return null; } /** * Remove the given editor from the hierarchy. * @param editorToRemove the editor to remove. * @param commit true to commit on remove, false to cancel. */ private void removeValueEditor(ValueEditor editorToRemove, boolean commit) { ValueEditorHierarchyManager vehm = getValueEditorHierarchyManager(); // Under some circumstances this method can be called twice. // Therefore only remove the value editor if it really is managed by the hierarchy manager. if (vehm.getTopValueEditors().contains(editorToRemove)) { vehm.removeValueEditor(editorToRemove, commit); } } /** * {@inheritDoc} */ public Object getCellEditorValue() { return valueEditor.getValueNode(); } /** * {@inheritDoc} */ public boolean isCellEditable(EventObject e) { if (e instanceof MouseEvent) { Point eventLocation = ((MouseEvent)e).getPoint(); // True if the cell corresponds to an argument. TreePath path = getPathForLocation(eventLocation.x, eventLocation.y); if (path != null) { return path.getLastPathComponent() instanceof ArgumentTreeNode.ArgumentNode; } } return false; } /** * {@inheritDoc} */ public boolean shouldSelectCell(EventObject anEvent) { return true; } /** * {@inheritDoc} */ public boolean stopCellEditing() { fireEditingStopped(); if (valueEditor != null) { removeValueEditor(valueEditor, true); } return true; } /** * {@inheritDoc} */ public void cancelCellEditing() { fireEditingCanceled(); if (valueEditor != null) { removeValueEditor(valueEditor, false); } } /** * {@inheritDoc} */ public void addCellEditorListener(CellEditorListener l) { listenerList.add(CellEditorListener.class, l); } /** * {@inheritDoc} */ public void removeCellEditorListener(CellEditorListener l) { listenerList.remove(CellEditorListener.class, l); } /** * Notify all listeners that have registered interest for notification on this event type. */ private void fireEditingStopped() { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == CellEditorListener.class) { ((CellEditorListener)listeners[i + 1]).editingStopped(changeEvent); } } } /** * Notify all listeners that have registered interest for notification on this event type. */ private void fireEditingCanceled() { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == CellEditorListener.class) { ((CellEditorListener)listeners[i + 1]).editingCanceled(changeEvent); } } } } /** * The class responsible for handling drag source-related updates. * @author Edward Lam */ private class DragSourceHandler extends DragSourceAdapter implements DragGestureListener { /** * {@inheritDoc} */ public void dragGestureRecognized(DragGestureEvent dragGestureEvent) { // if the explorer is not enabled for dragging (ie run mode) we just quit if (!isMouseEnabled()) { return; } // todoEL: Add drag image for this drag gesture // Find the proper node Point dragOrigin = dragGestureEvent.getDragOrigin(); TreePath pathToNode = getPathForLocation(dragOrigin.x, dragOrigin.y); if (pathToNode == null) { return; } ArgumentTreeNode treeNode = (ArgumentTreeNode)pathToNode.getLastPathComponent(); Rectangle pathBounds = getPathBounds(pathToNode); Point mousePointOffset = new Point(dragOrigin.x - pathBounds.x, dragOrigin.y - pathBounds.y); // If we are dragging the node, start a drag gesture if recognizable. if (treeNode instanceof ArgumentTreeNode.ArgumentNode) { ArgumentTreeNode.ArgumentNode argumentNode = (ArgumentTreeNode.ArgumentNode)treeNode; dragGestureEvent.startDrag(null, null, mousePointOffset, new InputTransferable(argumentNode.getArgument()), this); draggingNode = treeNode; } else if (treeNode instanceof ArgumentTreeNode.CollectorNode) { // What to do? Just create a string for now... ArgumentTreeNode.CollectorNode collectorNode = (ArgumentTreeNode.CollectorNode)treeNode; dragGestureEvent.startDrag(null, null, mousePointOffset, new StringSelection(collectorNode.getCollectorGem().getUnqualifiedName()), this); draggingNode = treeNode; } } /** * {@inheritDoc} */ @Override public void dragDropEnd(DragSourceDropEvent dsde) { draggingNode = null; } } /** * The instance of HighlightTreeDnDHandler for this argument tree. * @author Edward Lam */ private class DnDHandler implements HighlightTreeDnDHandler { /** * {@inheritDoc} */ public TreePath getPathForDrop(DropTargetDragEvent dtde) { // Can drop on the node if it's a collector node. Point dragLocation = dtde.getLocation(); TreePath pathUnder = getClosestPathForLocation(dragLocation.x, dragLocation.y); if (pathUnder != null) { Object lastPathComponent = pathUnder.getLastPathComponent(); if (lastPathComponent instanceof ArgumentTreeNode.CollectorNode) { return pathUnder; } else if (lastPathComponent instanceof ArgumentTreeNode.ArgumentNode) { // Two ways to adjust: // If we're dragging an unused argument around its target, highlight its collector. // If we're dragging a targeting argument around the unused argument part of its collector, do the same. // TODOEL: it would be nice to be able to figure out if an argument retargeting from another collector // would be unused. // Make a copy of the dragging node in case it changes.. ArgumentTreeNode draggingNode = ArgumentTree.this.draggingNode; // If not currently dragging an input, return the path. if (!(draggingNode instanceof ArgumentTreeNode.ArgumentNode)) { return null; } ArgumentTreeNode.ArgumentNode draggingArgumentNode = (ArgumentTreeNode.ArgumentNode)draggingNode; // Check if the node being dragged currently an unused argument on that node's target. // If there's no collector relevant to the path, return null. TreePath collectorNodePath = getCollectorNodePath(pathUnder); if (collectorNodePath == null) { return null; } // Get the collector node, and its reflected inputs. ArgumentTreeNode.CollectorNode collectorNode = (ArgumentTreeNode.CollectorNode)collectorNodePath.getLastPathComponent(); List<PartInput> reflectedInputs = collectorNode.getCollectorGem().getReflectedInputs(); // If it's dragging around the unused argument area of a collector's arguments, return the collector. ArgumentTreeNode.ArgumentNode argumentNodeUnder = (ArgumentTreeNode.ArgumentNode)lastPathComponent; if (!reflectedInputs.contains(argumentNodeUnder.getArgument())) { return collectorNodePath; } // If it's not an argument to this collector, return null. if (draggingArgumentNode.getParent() != collectorNode) { return null; } // It's an argument to this collector. // If it's not a reflected argument, return the collector. if (!reflectedInputs.contains(draggingArgumentNode.getArgument())) { return collectorNodePath; } // It's a reflected argument. return null; } } return null; } /** * {@inheritDoc} */ public boolean canAcceptDataFlavor(DataFlavor[] flavors) { // For now, only accept input drags. return Arrays.asList(flavors).contains(SingleInputDataFlavor.getSingleInputDataFlavor()); } /** * {@inheritDoc} */ public int getDropActions(DataFlavor[] flavors, int dropAction, TreePath path, Point location) { // if the explorer is not ready, then we reject all drag sources! if (!isMouseEnabled()) { return DnDConstants.ACTION_NONE; } // Check that the dataflavor of the transferable is supported. if (!canAcceptDataFlavor(flavors)) { return DnDConstants.ACTION_NONE; } // Get the collector node path associated with the drag location. TreePath collectorNodePath = getCollectorNodePath(path); // Reject if there is no associated collector. if (collectorNodePath == null) { return DnDConstants.ACTION_NONE; } // Get the collector gem from the associated collector node. ArgumentTreeNode lastPathComponent = (ArgumentTreeNode)collectorNodePath.getLastPathComponent(); CollectorGem collectorGem = (CollectorGem)lastPathComponent.getUserObject(); // Ensure the drag is acceptable for (final DataFlavor dataFlavor : flavors) { if (dataFlavor.equals(SingleInputDataFlavor.getSingleInputDataFlavor())) { SingleInputDataFlavor singleInputDataFlavor = (SingleInputDataFlavor)dataFlavor; // Accept only if the argument can be targeted at the collector. if (singleInputDataFlavor.canTarget(collectorGem)) { return DnDConstants.ACTION_MOVE; } else { return DnDConstants.ACTION_NONE; } } } // An acceptable data flavor was not found. return DnDConstants.ACTION_NONE; } /** * {@inheritDoc} */ public boolean doDrop(Transferable transferable, Point mousePoint, TreePath path, boolean forceDialog) { // This should be a drop on a collector node. // Get the current node targeted for the drop. Object lastPathComponent = path.getLastPathComponent(); // Abort if it's not a collector node. if (!(lastPathComponent instanceof ArgumentTreeNode.CollectorNode)) { return false; } CollectorGem collectorGem = ((ArgumentTreeNode.CollectorNode)lastPathComponent).getCollectorGem(); if (transferable.isDataFlavorSupported(SingleInputDataFlavor.getSingleInputDataFlavor())) { try { // Grab the input. Gem.PartInput input = (Gem.PartInput)transferable.getTransferData(SingleInputDataFlavor.getSingleInputDataFlavor()); // Abort if the input can't be targeted at the given collector gem. if (!SingleInputDataFlavor.canTarget(input, collectorGem)) { return false; } // Do nothing if the input is already targeted at the given collector gem. But pretend the drop succeeded. if (collectorGem.getTargetArguments().contains(input)) { return true; } // Retarget the argument, and select it in the tree. getOwner().retargetInputArgument(input, collectorGem, -1); selectInput(input); // Success! return true; } catch (UnsupportedFlavorException e) { e.printStackTrace(); return false; } catch (IOException e) { e.printStackTrace(); return false; } } return false; } /** * {@inheritDoc} */ public boolean doDrop(Transferable transferable, TreePath dropNodePath, boolean upperHalf, boolean onIcon) { // Get the current node targeted for the drop. TreePath collectorNodePath = getCollectorNodePath(dropNodePath); // Abort if there was no associated collector node. if (collectorNodePath == null) { return false; } ArgumentTreeNode.CollectorNode collectorNode = (ArgumentTreeNode.CollectorNode)collectorNodePath.getLastPathComponent(); CollectorGem collectorGem = collectorNode.getCollectorGem(); if (transferable.isDataFlavorSupported(SingleInputDataFlavor.getSingleInputDataFlavor())) { try { // Grab the input. Gem.PartInput input = (Gem.PartInput)transferable.getTransferData(SingleInputDataFlavor.getSingleInputDataFlavor()); // Abort if the input can't be targeted at the given collector gem. if (!SingleInputDataFlavor.canTarget(input, collectorGem)) { return false; } // Communicate to the owner that the input should be transferred to its new location. // Figure out the location at which the drop took place. ArgumentTreeNode lastPathComponent = (ArgumentTreeNode)dropNodePath.getLastPathComponent(); // Calculate what this corresponds to in terms of the new argument index. int addIndex; if (lastPathComponent instanceof ArgumentTreeNode.CollectorNode) { addIndex = -1; } else if (lastPathComponent instanceof ArgumentTreeNode.ArgumentNode) { ArgumentTreeNode.ArgumentNode argumentNode = (ArgumentTreeNode.ArgumentNode)lastPathComponent; // Get the index of the argument with respect to its collector's arguments. // The add index is the index of the drop argument, modified a bit.. addIndex = collectorGem.getTargetArguments().indexOf(argumentNode.getArgument()); // If the drop location is in the upper half of the bounds, the drop argument should be added before it. // Otherwise, it should be added after. if (!upperHalf) { addIndex++; } // Adjust for the case where the input is being retargeted to a higher location on the same collector. if (collectorGem == GemGraph.getInputArgumentTarget(input) && collectorGem.getReflectedInputs().indexOf(input) < addIndex) { addIndex--; } } else { throw new IllegalStateException("Unrecognized drop target class: " + lastPathComponent.getClass()); } // Now retarget the argument, and select it in the tree. getOwner().retargetInputArgument(input, collectorGem, addIndex); selectInput(input); // Success! return true; } catch (UnsupportedFlavorException e) { e.printStackTrace(); return false; } catch (IOException e) { e.printStackTrace(); return false; } } return false; } } /** * Constructor for an ArgumentTree * @param gemGraph the related GemGraph * @param owner */ ArgumentTree(GemGraph gemGraph, ArgumentExplorerOwner owner) { this.owner = owner; // Set our custom model. ArgumentTreeNode.RootNode root = new ArgumentTreeNode.RootNode(); ArgumentTreeModel argumentTreeModel = new ArgumentTreeModel(gemGraph, root); // Changes are commited on edit interrupt. setInvokesStopCellEditing(true); // Ensure that inserted nodes are expanded. // Note that we add the listener before any other listener can be added. // This is because the JTree adds its own listeners to update itself on model change, but any added nodes will be // collapsed by default. The model listeners are notified in *reverse* order from the order in which they are added, // meaning that if we add this listener first, it will be notified last, after any new nodes have appeared. argumentTreeModel.addTreeModelListener(new TreeModelListener() { public void treeNodesChanged(TreeModelEvent e) { expandAll(e.getTreePath(), true); } public void treeNodesInserted(TreeModelEvent e) { expandAll(e.getTreePath(), true); } public void treeNodesRemoved(TreeModelEvent e) { } public void treeStructureChanged(TreeModelEvent e) { expandAll(e.getTreePath(), true); } }); setModel(argumentTreeModel); // Set our custom renderer. setCellRenderer(cellRenderer); // Don't show the root, or root handles for expand/collapse. setRootVisible(false); setShowsRootHandles(false); // Register with the tooltip manager. ToolTipManager.sharedInstance().registerComponent(this); // Allow single selection only.. (for now..) DefaultTreeSelectionModel treeSelectionModel = new DefaultTreeSelectionModel(); treeSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); setSelectionModel(treeSelectionModel); // Create the drag gesture recognizer for this tree. DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_MOVE, new DragSourceHandler()); // Veto collapsing (for now); addTreeWillExpandListener(new TreeWillExpandListener() { public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { throw new ExpandVetoException(event); } public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { } }); // Ensure that the initial view is fully expanded. treeDidChange(); expandRow(0); updateUI(); // Set the row height to a negative value so that the cell renderer is used to query for each row's height. setRowHeight(-1); } /** * @return the ArgumentTreeModel backing this tree. */ ArgumentTreeModel getArgumentTreeModel() { return (ArgumentTreeModel)getModel(); } /** * {@inheritDoc} */ @Override protected HighlightTreeDnDHandler getHighlightTreeDnDHandler() { return dndHandler; } /** * {@inheritDoc} */ @Override public String getToolTipText(MouseEvent e) { TreePath treePath = getPathForLocation(e.getX(), e.getY()); if (treePath == null) { return null; } ArgumentTreeNode node = (ArgumentTreeNode) treePath.getLastPathComponent(); Object userObject = node.getUserObject(); if (userObject instanceof Gem.PartInput) { return owner.getHTMLFormattedMetadata((Gem.PartInput) userObject); } else if (userObject instanceof CollectorGem) { // Start with the open html tag String toolTip = "<html>"; // If available add the metadata short description FunctionMetadata metadata = ((CollectorGem)userObject).getDesignMetadata(); String shortDescription = metadata.getShortDescription(); if (shortDescription != null && shortDescription.length() > 0) { toolTip += "<p>" + shortDescription + "</p>"; } // Stick in the result type information toolTip += GemCutterMessages.getString("ResultTypeToolTip", owner.getTypeString(((CollectorGem) userObject).getResultType())); // End with the end html tag toolTip += "</html>"; return toolTip; } return null; } /** * @return any node currently being dragged as a transferable. Null if none. */ private ArgumentTreeNode getDraggingNode() { return draggingNode; } /** * Expand or collapse all nodes under a given path. * @param parentPath the path to the node under which nodes will be expanded or collapsed. * @param expand true to expand, false to collapse. */ private void expandAll(TreePath parentPath, boolean expand) { // Expansion or collapsing must be done from the bottom-up. // Traverse children first. TreeNode parentNode = (TreeNode)parentPath.getLastPathComponent(); for (Enumeration<TreeNode> e = UnsafeCast.unsafeCast(parentNode.children()); e.hasMoreElements(); ) { TreeNode nextChildNode = e.nextElement(); TreePath childPath = parentPath.pathByAddingChild(nextChildNode); expandAll(childPath, expand); } // Now deal with the parent. if (expand) { expandPath(parentPath); } else { collapsePath(parentPath); } } /** * @return the owner. */ public ArgumentExplorerOwner getOwner() { return owner; } /** * Display the controls for argument entry. * @param inputToArgumentPanelMap a map from input to a value entry panel editing its argument value, * for those inputs for which the currently running gem requires arguments. * An iterator on this map should return the inputs in the correct order. */ public void showArgumentControls(Map<PartInput, ValueEditor> inputToArgumentPanelMap) { cellRenderer.showArgumentValues(inputToArgumentPanelMap); setCellEditor(new CellEditor(inputToArgumentPanelMap)); setRowHeight(ValueEntryPanel.PANEL_HEIGHT); setEditable(true); // This seems to be the only way to get the tree to repaint itself properly. getArgumentTreeModel().nodeStructureChanged((ArgumentTreeNode)getArgumentTreeModel().getRoot()); } /** * Hide the controls for argument entry. */ public void hideArgumentControls() { cellRenderer.showArgumentValues(null); setRowHeight(-1); setEditable(false); getArgumentTreeModel().nodeStructureChanged((ArgumentTreeNode)getArgumentTreeModel().getRoot()); } /** * Enables and disables the mouse events (such that the explorer can be used purely as a view). * @param enabled whether to enable mouse events */ void enableMouseInputs(boolean enabled) { // Skip out earlier if the state hasn't changed. This is important since we don't want to add // back the mouse listeners everytime this method is called, we only want to add them once when // the run state changes from disabled to enabled. if (mouseInputEnabled == enabled) { return; } this.mouseInputEnabled = enabled; } /** * Returns the current status of the explorer * @return boolean */ public boolean isMouseEnabled() { return mouseInputEnabled; } /** * Select the given input in the tree. * @param inputToSelect the input to select */ void selectInput(Gem.PartInput inputToSelect) { ArgumentTreeNode.ArgumentNode argumentNode = getArgumentTreeModel().getArgumentNode(inputToSelect); setSelectionPath(new TreePath(argumentNode.getPath())); } /** * Get the collector node path which is relevant to a given path * @param treePath the path in question. * @return the collector node path related to the given tree path. */ TreePath getCollectorNodePath(TreePath treePath) { if (treePath == null) { return null; } Object lastPathComponent = treePath.getLastPathComponent(); // If it's a collector node, return that node. if (lastPathComponent instanceof ArgumentTreeNode.CollectorNode) { return treePath; } // If it's an argument node, return the parent path (which should be a path to the targeted collector). if (lastPathComponent instanceof ArgumentTreeNode.ArgumentNode) { return treePath.getParentPath(); } return null; } /** * {@inheritDoc} */ @Override protected boolean canDropOnIcon(int row) { return false; } }