/*
* 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.
*/
/*
* TableTopExplorer.java
* Creation date: Dec 21st 2002
* By: Ken Wong
*/
package org.openquark.gems.client.explorer;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.dnd.DropTarget;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.undo.UndoableEdit;
import org.openquark.gems.client.BurnEvent;
import org.openquark.gems.client.BurnListener;
import org.openquark.gems.client.CodeGem;
import org.openquark.gems.client.CodeGemDefinitionChangeEvent;
import org.openquark.gems.client.CodeGemDefinitionChangeListener;
import org.openquark.gems.client.CodeGemEditor;
import org.openquark.gems.client.CollectorGem;
import org.openquark.gems.client.Connection;
import org.openquark.gems.client.FunctionalAgentGem;
import org.openquark.gems.client.Gem;
import org.openquark.gems.client.GemGraphAdditionEvent;
import org.openquark.gems.client.GemGraphChangeListener;
import org.openquark.gems.client.GemGraphConnectionEvent;
import org.openquark.gems.client.GemGraphDisconnectionEvent;
import org.openquark.gems.client.GemGraphRemovalEvent;
import org.openquark.gems.client.InputChangeEvent;
import org.openquark.gems.client.InputChangeListener;
import org.openquark.gems.client.NameChangeEvent;
import org.openquark.gems.client.NameChangeListener;
import org.openquark.gems.client.RecordCreationGem;
import org.openquark.gems.client.RecordFieldSelectionGem;
import org.openquark.gems.client.ReflectorGem;
import org.openquark.gems.client.ValueGem;
import org.openquark.gems.client.ValueGemChangeEvent;
import org.openquark.gems.client.ValueGemChangeListener;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.valueentry.MetadataRunner;
import org.openquark.gems.client.valueentry.ValueEditorDirector;
import org.openquark.gems.client.valueentry.ValueEditorHierarchyManager;
/**
* This gem explorer presents a tree-based interface for viewing and manipulating the gem graph. Note that this is currently,
* primarily a UI class. The actual manipulation of the gem graph is delegated to the 'TableTopExplorerOwner'. We implement
* GemGraphChangeListener, NameChangeListener, CodeGemDefinitionChangeListener, so that we can keep up with changes to the GemGraph.
* @author Ken Wong
*/
public class TableTopExplorer extends JComponent implements GemGraphChangeListener {
private static final long serialVersionUID = -4416110823508003865L;
/** The namespace for log messages from this package. */
public static final String EXPLORER_LOGGER_NAMESPACE = TableTopExplorer.class.getPackage().getName();
/** An instance of a Logger for messages from the explorer package. */
static final Logger EXPLORER_LOGGER = Logger.getLogger(EXPLORER_LOGGER_NAMESPACE);
/** The owner of the explorer, which completes most of the heavy duty work. */
private TableTopExplorerOwner explorerOwner;
/** The tree that is currently displayed. Note that this tree is reconstructed at each update */
private ExplorerTree explorerTree;
/** Whether or not mouse input for the tree is enabled. */
private boolean mouseInputEnabled = true;
/** Mouse listeners added to the tree before mouse input was disabled. */
private MouseListener[] savedMouseListeners = new MouseListener[0];
/** Mouse motion listeners added to the tree before mouse input was disabled. */
private MouseMotionListener[] savedMouseMotionListeners = new MouseMotionListener[0];
/** A listener which updates the tree on various gem events. */
private final GemChangeListener gemChangeListener = new GemChangeListener();
/** The root node of the TableTopExplorer tree */
private ExplorerRootNode explorerTreeRoot;
/**
* Map from Gem to its tree node.
*/
private Map<Gem, ExplorerGemNode> gemToNodesMap;
/**
* Map from a PartInput to its tree node.
*/
private Map<PartInput, ExplorerInputNode> inputsToNodeMap;
/**
* If the selection in the explorer tree changes this is the old selection path.
* If there is no old selection path it will equal the new/current path.
*/
private TreePath oldSelectionPath = null;
/** The ValueEditorHierarchyManager for editors in the explorer.*/
private ValueEditorHierarchyManager valueEditorHierarchyManager;
/** Wraps the explorer tree to ensure proper scrolling */
private final JScrollPane treeScrollPane;
/**
* This listener selects the appropriate gem on the table top, if the user
* selects a gem in the tree.
*/
private TreeSelectionListener treeSelectionListener = new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
if (explorerTree.isEditing()) {
explorerTree.cancelEditing();
return;
}
oldSelectionPath = e.getOldLeadSelectionPath();
TreePath newSelection = e.getNewLeadSelectionPath();
if (oldSelectionPath == null) {
oldSelectionPath = newSelection;
}
if (newSelection == null) {
explorerOwner.selectGem(null, true);
} else {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)newSelection.getLastPathComponent();
// unfortunately, removing a node causes it to be selected in a value changed event!
if (!node.getRoot().equals(explorerTree.getModel().getRoot())) {
return;
}
if (node instanceof ExplorerGemNode) {
ExplorerGemNode gemNode = (ExplorerGemNode) node;
explorerOwner.selectGem(gemNode.getGem(), true);
} else if (node instanceof ExplorerInputNode) {
ExplorerInputNode inputNode = (ExplorerInputNode) node;
explorerOwner.selectGem(inputNode.getPartInput().getGem(), true);
} else {
explorerOwner.selectGem(null, true);
}
if (newSelection.getLastPathComponent() != explorerTreeRoot) {
explorerTree.scrollPathToVisible(newSelection);
}
}
}
};
/**
* This listener displays the explorer popup menu if the right-mouse button is pressed.
*/
private MouseListener mouseListener = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
maybeShowPopupMenu(e);
}
@Override
public void mouseReleased(MouseEvent e) {
maybeShowPopupMenu(e);
}
/**
* Displays the popup menu for a mouse event if the event is a popup trigger.
* @param e the MouseEvent
*/
private void maybeShowPopupMenu(MouseEvent e) {
if (e.isPopupTrigger()) {
TreePath treePath = getExplorerTree().getPathForLocation(e.getX(), e.getY());
// If the right-click was not on one of the selected items, then change
// the selection to be the item at the click location (if any).
// This will allow multiple-selection to be preserved when right-clicking.
if (treePath == null || !getExplorerTree().isPathSelected(treePath)) {
getExplorerTree().setSelectionPath(treePath);
}
JPopupMenu popupMenu = getPopup(treePath);
if (popupMenu != null) {
popupMenu.show(getExplorerTree(), e.getX(), e.getY());
}
}
}
/**
* Gets the correct popup menu from the explorer owner.
* @param triggeredPath the path to the tree node on which the context menu was invoked (may be null)
* @return JPopupMenu the popup menu
*/
private JPopupMenu getPopup(TreePath triggeredPath) {
// Get the current selection path.
TreePath[] treePaths = getExplorerTree().getSelectionPaths();
if (treePaths == null || treePaths.length == 0) {
return explorerOwner.getPopupMenu();
}
// Build a list of the selected nodes and check whether all the nodes are the same type.
Set<Gem> selectedGems = new LinkedHashSet<Gem>();
Set<PartInput> selectedInputs = new LinkedHashSet<PartInput>();
boolean allGemNodes = true;
boolean allInputNodes = true;
for (int nodeN = 0, nNodes = treePaths.length; nodeN < nNodes; ++nodeN) {
Object selectedNode = treePaths[nodeN].getLastPathComponent();
if (!(selectedNode instanceof ExplorerGemNode)) {
allGemNodes = false;
}
if (!(selectedNode instanceof ExplorerInputNode)) {
allInputNodes = false;
}
if (selectedNode instanceof ExplorerGemNode) {
ExplorerGemNode gemNode = (ExplorerGemNode) selectedNode;
selectedGems.add(gemNode.getGem());
}
else if (selectedNode instanceof ExplorerInputNode) {
ExplorerInputNode inputNode = (ExplorerInputNode) selectedNode;
selectedInputs.add(inputNode.getPartInput());
}
}
// If a mixture of inputs and gems are selected, then change the selection to be
// the triggered item and determine menu based on this selection.
if (triggeredPath != null) {
Object triggeredNode = triggeredPath.getLastPathComponent();
if ((triggeredNode instanceof ExplorerGemNode && !allGemNodes)
|| (triggeredNode instanceof ExplorerInputNode && !allInputNodes)) {
getExplorerTree().setSelectionPath(triggeredPath);
getPopup(triggeredPath);
}
}
// Get the correct popup menu for the type of node.
if (allGemNodes) {
return explorerOwner.getPopupMenu(selectedGems.toArray(new Gem[0]));
}
else if (allInputNodes) {
return explorerOwner.getPopupMenu(selectedInputs.toArray(new Gem.PartInput[0]));
}
else {
return explorerOwner.getPopupMenu();
}
}
};
/**
* A listener on various gem change events.
* @author Edward Lam
*/
private class GemChangeListener
implements NameChangeListener, InputChangeListener, ValueGemChangeListener, BurnListener,
CodeGemDefinitionChangeListener, CodeGemEditor.EditHandler {
/**
* {@inheritDoc}
*/
public void burntStateChanged(BurnEvent e) {
handleBurnStateChange((Gem.PartInput)e.getSource());
}
/**
* {@inheritDoc}
*/
public void definitionEdited (CodeGemEditor codeGemEditor, Gem.PartInput[] oldInputs, UndoableEdit codeGemEdit) {
handleCodeGemDefinitionEdit();
}
/**
* {@inheritDoc}
*/
public void nameChanged(NameChangeEvent e) {
handleNameChange((Gem)e.getSource());
}
/**
* {@inheritDoc}
*/
public void valueChanged(ValueGemChangeEvent e) {
handleValueChange((ValueGem)e.getSource());
}
/**
* {@inheritDoc}
*/
public void inputsChanged(InputChangeEvent e) {
handleInputsChanged((Gem)e.getSource());
}
/**
* {@inheritDoc}
* TODOEL: necessary? inputsChanged() will also be called.
*/
public void codeGemDefinitionChanged(CodeGemDefinitionChangeEvent e) {
handleCodeGemDefinitionChange((CodeGem)e.getSource());
}
}
/**
* Default constructor for the TableTopExplorer.
* @param explorerOwner the owner of this table top explorer
* @param rootNodeName the name of the root node
*/
public TableTopExplorer(TableTopExplorerOwner explorerOwner, String rootNodeName) {
this(explorerOwner, rootNodeName, null, null);
}
/**
* Default constructor for the TableTopExplorer.
* @param explorerOwner the owner of this table top explorer
* @param rootNodeName the name of the root node
* @param navigationHelper A helper for the explorer tree to control whether nodes can be focussed on
* @param cellRenderer A customized cell renderer. If this is null then the default renderer will be
* used
*/
public TableTopExplorer(TableTopExplorerOwner explorerOwner,
String rootNodeName,
ExplorerNavigationHelper navigationHelper,
DefaultTreeCellRenderer cellRenderer) {
if (explorerOwner == null || rootNodeName == null) {
throw new NullPointerException();
}
this.explorerOwner = explorerOwner;
gemToNodesMap = new HashMap<Gem, ExplorerGemNode>();
inputsToNodeMap = new HashMap<PartInput, ExplorerInputNode>();
explorerTreeRoot = new ExplorerRootNode(rootNodeName);
explorerTree = new ExplorerTree(explorerTreeRoot, explorerOwner, this, navigationHelper, cellRenderer);
explorerTree.addTreeSelectionListener(treeSelectionListener);
explorerTree.addMouseListener(mouseListener);
explorerTree.setDropTarget(new DropTarget(explorerTree, new ExplorerDragAndDropHandler(this)));
explorerTree.setAutoscrolls(true);
// Create a scroll pane around the tree
treeScrollPane = new JScrollPane(explorerTree);
treeScrollPane.setPreferredSize(new Dimension(500,500));
// Add the scroll pane as the centre component for the table top explorer
this.setLayout(new BorderLayout());
this.add(treeScrollPane, BorderLayout.CENTER);
rebuildTree();
}
/**
* Scrolls the explorer tree to make the desired bounds visible. This is done by calling
* scrollRectToVisible() on the explorer trees scroll pane viewpoint.
* @param bounds Rectangle - The rectangle that should be visible after scrolling.
*/
public void scrollExplorerTreeToVisible(Rectangle bounds) {
treeScrollPane.getViewport().scrollRectToVisible(bounds);
}
/**
* Builds the tree from the roots.
* @param roots the set of root nodes
* @return JTree the explorer tree
*/
public JTree growTrees(Set<Gem> roots) {
List<Gem> sortedRoots = new ArrayList<Gem>(roots);
explorerTreeRoot.removeAllChildren();
// Iterate through the set of roots and grow the trees
for (final Gem root : sortedRoots) {
explorerTreeRoot.add(growTree(root));
}
getExplorerTree().setVisible(true);
return getExplorerTree();
}
/**
* Creates a new explorer node and prepares the node appropriately by adding various listeners to keep the node
* updated. This function should always be used in place of the ExplorerGemNode constructor if the created node
* is to be used in the TableTopExplorer.
* @param gem the gem for which to create the node
* @return the new node
*/
private ExplorerGemNode createNewExplorerGemNode(Gem gem) {
gem.addInputChangeListener(gemChangeListener);
if (gem instanceof CollectorGem) {
gem.addNameChangeListener(gemChangeListener);
} else if (gem instanceof CodeGem) {
gem.addNameChangeListener(gemChangeListener);
gem.addInputChangeListener(gemChangeListener);
((CodeGem)gem).addDefinitionChangeListener(gemChangeListener);
} else if (gem instanceof RecordFieldSelectionGem) {
gem.addNameChangeListener(gemChangeListener);
} else if (gem instanceof RecordCreationGem) {
gem.addInputChangeListener(gemChangeListener);
gem.addNameChangeListener(gemChangeListener);
} else if (gem instanceof ReflectorGem) {
gem.addInputChangeListener(gemChangeListener);
} else if (gem instanceof ValueGem) {
((ValueGem)gem).addValueChangeListener(gemChangeListener);
} else if (gem instanceof FunctionalAgentGem) {
((FunctionalAgentGem)gem).addNameChangeListener(gemChangeListener);
}
gem.addBurnListener(gemChangeListener);
ExplorerGemNode newNode = new ExplorerGemNode(gem);
gemToNodesMap.put(gem, newNode);
return newNode;
}
/**
* Creates a new ExplorerInputNode and preps it for the tree.
* @param input the input to create the node for
* @return the new node
*/
private ExplorerInputNode createNewExplorerGemNode(Gem.PartInput input) {
ExplorerInputNode newNode = new ExplorerInputNode(input);
inputsToNodeMap.put(input, newNode);
return newNode;
}
/**
* Fires a node structure changed event to the tree model and saves/restores the
* tree state before and after firing the event.
* @param node the node that has changed
*/
private void fireNodeStructureChanged(TreeNode node) {
getExplorerTree().saveState();
getExplorerTreeModel().nodeStructureChanged(node);
getExplorerTree().restoreSavedState();
}
/**
* Expands the given node.
* @param node the node to be expanded
*/
private void expandNode(DefaultMutableTreeNode node) {
TreePath path = new TreePath(node.getPath());
explorerTree.expandPath(path);
}
/**
* A recursive method that grows each individual gem tree.
* @param gem the gem that is the root of the tree to grow
* @return ExplorerGemNode the root node for the tree
*/
ExplorerGemNode growTree(Gem gem) {
ExplorerGemNode node = gemToNodesMap.containsKey(gem) ? gemToNodesMap.get(gem) : createNewExplorerGemNode(gem);
// We want to show the proper gem inputs too.
Gem.PartInput[] inputs = gem.getInputParts();
for (final PartInput input : inputs) {
if (input.isConnected()) {
Gem sourceGem = input.getConnection().getSource().getGem();
node.add(growTree(sourceGem));
} else {
ExplorerInputNode mutableTreeNode = createNewExplorerGemNode(input);
node.add(mutableTreeNode);
}
}
return node;
}
/**
* Called to grow a tree or nodes starting from a part input rather than a gem.
* @param input the input from which to grow the tree
* @return ExplorerGemNode the root node for the tree
*/
ExplorerInputNode growTree(PartInput input) {
ExplorerInputNode node = createNewExplorerGemNode(input);
// We want to show the proper descendants if there are any
if (input.isConnected()) {
node.add(growTree(input.getConnection().getSource().getGem()));
}
return node;
}
/**
* @see org.openquark.gems.client.GemGraphChangeListener#gemRemoved(GemGraphRemovalEvent)
*/
public void gemRemoved(GemGraphRemovalEvent e) {
Gem gem = (Gem)e.getSource();
// If we want to remove a gem, we need to remove all of its inputs from our cache
// we also need to add its dependents onto the root. (We shouldn't really have to, since the gemGraph should've done
// disconnections before the deletion. but just to be safe
ExplorerGemNode node = gemToNodesMap.get(gem);
// This is possible, in the case of unconnected emitters and thus is nothing to worry about.
if (node == null) {
return;
}
for (int i = 0; i < node.getChildCount(); i++) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode)node.getChildAt(i);
// So if it was an input, we remove it from our map, if it's a gem, then we disconect it by adding
// it onto the root
if (child instanceof ExplorerInputNode) {
ExplorerInputNode inputNode = (ExplorerInputNode) child;
inputsToNodeMap.remove(inputNode.getPartInput());
i--;
} else {
explorerTreeRoot.add(child);
}
child.removeFromParent();
}
// We need to recreate the input of this node's parent (There was no input node because that spot was taken up by this gem
// we go through the inputs and if the input doesn't exist, we create it.
DefaultMutableTreeNode parent =(DefaultMutableTreeNode)node.getParent();
if (parent instanceof ExplorerGemNode) {
ExplorerGemNode explorerGemNode = (ExplorerGemNode) parent;
Gem parentGem = explorerGemNode.getGem();
// cycle through the input parts and see if they are all there.
Gem.PartInput[] inputs = parentGem.getInputParts();
for (int i = 0; i < inputs.length; i++) {
// we create a new input only if there's supposed to be an input there!
if (!inputsToNodeMap.containsKey(inputs[i]) && !inputs[i].isConnected()) {
ExplorerInputNode mutableTreeNode = createNewExplorerGemNode(inputs[i]);
parent.add(mutableTreeNode);
}
}
}
if (parent != null) {
parent.remove(node);
}
gemToNodesMap.remove(gem);
fireNodeStructureChanged(explorerTreeRoot);
}
/**
* @see org.openquark.gems.client.GemGraphChangeListener#gemDisconnected(GemGraphDisconnectionEvent)
*/
public void gemDisconnected(GemGraphDisconnectionEvent e) {
Connection connection = (Connection)e.getSource();
Gem sourceGem = connection.getSource().getGem();
Gem destinationGem = connection.getDestination().getGem();
ExplorerGemNode sourceGemNode = gemToNodesMap.get(sourceGem);
ExplorerGemNode destinationGemNode = gemToNodesMap.get(destinationGem);
// This scope may not be showing the gem that triggered the event so there may be nothing to do.
if (sourceGemNode == null) {
return;
}
// We add the disconnected node (the source gem) onto the root.
sourceGemNode.removeFromParent();
explorerTreeRoot.add(sourceGemNode);
// Add the missing input to the destination gem's node.
Gem.PartInput[] inputs = destinationGem.getInputParts();
for (int i = 0; i < inputs.length; i++) {
if (!inputsToNodeMap.containsKey(inputs[i]) && !inputs[i].isConnected()) {
ExplorerInputNode inputNode = createNewExplorerGemNode(inputs[i]);
inputsToNodeMap.put(inputs[i], inputNode);
destinationGemNode.insert(inputNode, i);
}
}
fireNodeStructureChanged(explorerTreeRoot);
expandNode(sourceGemNode);
}
/**
* @see org.openquark.gems.client.GemGraphChangeListener#gemConnected(GemGraphConnectionEvent)
*/
public void gemConnected(GemGraphConnectionEvent e) {
Connection connection = (Connection)e.getSource();
Gem.PartInput destinationInput = connection.getDestination();
ExplorerGemNode sourceGemNode = gemToNodesMap.get(connection.getSource().getGem());
ExplorerGemNode destinationGemNode = gemToNodesMap.get(destinationInput.getGem());
ExplorerInputNode destinationInputNode = inputsToNodeMap.get(destinationInput);
// If the source gem is not being shown by the explorer then there is nothing to do.
if (sourceGemNode == null) {
return;
}
// Remove the source gem from it's parent.
sourceGemNode.removeFromParent();
// Remove the previously unconnected input node. It may have been removed already!
// This is done for a special reason
// in a connection to a code gem, where the type of the expression was ambiguous,
// a definition update event may be generated, and in fact, it is generated before the connection
// event is posted, but after the connection has been bound. And so, in such cases,
// the source gem must be disconnected from the root, or whatever it was connected to,
// before continuing. Consequently, we may not have to disconnect here, as it might have been done
// already.
if (destinationInputNode != null) {
destinationInputNode.removeFromParent();
inputsToNodeMap.remove(destinationInput);
}
// Put the gem node in place of the old input node.
int childIndex = destinationInput.getInputNum();
destinationGemNode.insert(sourceGemNode, childIndex);
fireNodeStructureChanged(explorerTreeRoot);
expandNode(sourceGemNode);
}
/**
* @see org.openquark.gems.client.GemGraphChangeListener#gemAdded(GemGraphAdditionEvent)
*/
public void gemAdded(GemGraphAdditionEvent e) {
handleGemAdded((Gem)e.getSource());
}
/**
* Handle the addition of a new gem to the tabletop.
* Creates the necessary nodes, and adds it onto the root node.
* @param gem the gem which was added.
*/
private void handleGemAdded(Gem gem) {
// Create the gem node.
ExplorerGemNode explorerGemNode = gemToNodesMap.containsKey(gem) ? gemToNodesMap.get(gem) : createNewExplorerGemNode(gem);
// Add the input nodes.
Gem.PartInput[] inputs = gem.getInputParts();
for (final PartInput input : inputs) {
ExplorerInputNode inputNode = createNewExplorerGemNode(input);
explorerGemNode.add(inputNode);
}
// Add the node to the tree, and notify.
explorerTreeRoot.add(explorerGemNode);
fireNodeStructureChanged(explorerTreeRoot);
expandNode(explorerGemNode);
}
/**
* Handle a change in the burn state of an input.
* @param input
*/
public void handleBurnStateChange(Gem.PartInput input) {
getExplorerTreeModel().nodeChanged(inputsToNodeMap.get(input));
}
/**
* Handle an edit to a code gem's definition.
*/
public void handleCodeGemDefinitionEdit() {
// Cancel any editing if the user changes a code gem definition via a code panel.
getExplorerTree().cancelEditing();
}
/**
* Handle a change in the name of a gem.
* @param gem
*/
public void handleNameChange(Gem gem) {
if (!explorerTree.isEditing()) {
refreshForRename(gem);
}
}
/**
* Handle a change in the value of a value gem.
* @param valueGem
*/
public void handleValueChange(ValueGem valueGem) {
ExplorerGemNode explorerGemNode = gemToNodesMap.get(valueGem);
// On deleting a value gem, the gem is removed, then its editor is closed. The editor commits on close,
// but the gem is already gone, meaning that the explorer gem node is no longer in the map.
if (explorerGemNode == null) {
return;
}
// Handle the text of the node changing.
getExplorerTreeModel().nodeChanged(explorerGemNode);
}
/**
* Handle a change in the inputs of a gem.
* @param gem
*/
public void handleInputsChanged(Gem gem) {
ExplorerGemNode explorerGemNode = gemToNodesMap.get(gem);
// Rebuild the node.
// TODOEL: do we need to rebuild on disconnect??
rebuildGemNode(explorerGemNode);
}
/**
* Handle a change in the definition of a code gem.
* @param codeGem
*/
public void handleCodeGemDefinitionChange(CodeGem codeGem) {
ExplorerGemNode explorerGemNode = gemToNodesMap.get(codeGem);
// If the definition changes, then the node may need to update to reflect brokenness
getExplorerTreeModel().nodeChanged(explorerGemNode);
}
/**
* Completely updates the tree structure by rebuilding the tree from the ground up
*/
public void rebuildTree() {
gemToNodesMap.clear();
inputsToNodeMap.clear();
Set<Gem> roots = explorerOwner.getRoots();
growTrees(roots);
getExplorerTreeModel().nodeStructureChanged(explorerTreeRoot);
// We expand everything at initially, because it looks better, and is generally what people want.
for (int i = 0; i < explorerTree.getRowCount(); i++) {
explorerTree.expandRow(i);
}
revalidate();
}
/**
* @return the explorer tree displayed by this table top explorer
*/
public ExplorerTree getExplorerTree() {
return explorerTree;
}
/**
* @return the tree model of the explorer tree
*/
public DefaultTreeModel getExplorerTreeModel() {
return (DefaultTreeModel) explorerTree.getModel();
}
/**
* Returns the tree node for a given gem.
* @param gem
* @return DefaultMutableTreeNode
*/
public DefaultMutableTreeNode getGemNode(Gem gem) {
return gemToNodesMap.get(gem);
}
/**
* @param input the input to get a node for
* @return the node for the input
*/
public DefaultMutableTreeNode getInputNode(Gem.PartInput input) {
return inputsToNodeMap.get(input);
}
/**
* @return the explorer owner of this table top explorer
*/
TableTopExplorerOwner getExplorerOwner() {
return explorerOwner;
}
/**
* Get the ValueEditorHierarchyManager for editors in the explorer.
* @return valueEditorHierarchyManager
*/
public ValueEditorHierarchyManager getValueEditorHierarchyManager() {
if (valueEditorHierarchyManager == null && explorerOwner.getValueEditorManager() != null) {
valueEditorHierarchyManager = new ValueEditorHierarchyManager(explorerOwner.getValueEditorManager());
// Set up the hierarchy so that hierarchy commit/cancel events close the top-level editor.
valueEditorHierarchyManager.setHierarchyCommitCancelHandler(new ValueEditorHierarchyManager.HierarchyCommitCancelHandler() {
public void handleHierarchyCommitCancel(boolean commit) {
if (commit) {
explorerTree.stopEditing();
} else {
explorerTree.cancelEditing();
}
}
});
}
return valueEditorHierarchyManager;
}
/**
* Get the MetadataRunner for editors in the explorer.
* @param gem The gem for which we wish to run metadata
* @return a metadata runner object
*/
public MetadataRunner getMetadataRunner(Gem gem) {
return explorerOwner.getMetadataRunner(gem);
}
/**
* Get the ValueEditorDirector for editors in the explorer.
* @return valueEditorDirector
*/
public ValueEditorDirector getValueEditorDirector() {
return explorerOwner.getValueEditorManager().getValueEditorDirector();
}
/**
* Refreshes the tree after a rename operation. If the node of the gem that was renamed is
* a child of the root node, this will re-sort all nodes in the root so that they still are
* in alphabetical order. Does nothing if the gem node is connected to an input.
* @param gem the gem that was renamed
*/
void refreshForRename(Gem gem) {
ExplorerGemNode node = gemToNodesMap.get(gem);
if (node.getParent() == explorerTreeRoot) {
node.removeFromParent();
explorerTreeRoot.add(node);
fireNodeStructureChanged(explorerTreeRoot);
}
}
/**
* Rebuilds the given gem node by removing and reading its inputs. This should be called if
* an inputs was added/removed or if the input order has changed.
* @param explorerGemNode the gem node to rebuild
*/
private void rebuildGemNode(ExplorerGemNode explorerGemNode) {
// We get rid of all the 'old' inputs
while (explorerGemNode.getChildCount() > 0) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) explorerGemNode.getFirstChild();
child.removeFromParent();
inputsToNodeMap.remove(child.getUserObject());
}
// And repopulate with the new ones. (provided that they are not connected)
Gem gem = explorerGemNode.getGem();
Gem.PartInput[] partInputs = gem.getInputParts();
for (final PartInput partInput : partInputs) {
if (partInput.isConnected()) {
// Due to the order that the events are handled, it is possible that the gem to which this input is
// connected has not been added to the tree yet.
// If this is the case, then a new node will be created for it (even though the tree will likely be
// rebuilt with the new structure afterwards anyway).
Gem sourceGem = partInput.getConnectedGem();
ExplorerGemNode source = gemToNodesMap.containsKey(sourceGem) ? gemToNodesMap.get(sourceGem) : createNewExplorerGemNode(sourceGem);
// This is done for a special reason
// in a connection to a code gem, where the type of the expression was ambiguous,
// a definition update event may be generated, and in fact, it is generated before the connection
// event is posted, but after the connection has been bound. And so, in such cases,
// the source gem must be disconnected from the root, or whatever it was connected to,
// before continuing.
explorerGemNode.add(source);
} else {
ExplorerInputNode newInputNode = createNewExplorerGemNode(partInput);
explorerGemNode.add(newInputNode);
}
}
fireNodeStructureChanged(explorerGemNode);
expandNode(explorerGemNode);
}
/**
* Enables and disables the mouse events (such that the explorer can be used purely as a view).
* @param enabled whether to enable mouse events
*/
public 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;
// Maybe this should be just disabled?
// explorerTree.setEnabled(enabled);
if (enabled) {
for (final MouseListener listener : savedMouseListeners) {
explorerTree.addMouseListener(listener);
}
for (final MouseMotionListener listener : savedMouseMotionListeners) {
explorerTree.addMouseMotionListener(listener);
}
} else {
savedMouseListeners = explorerTree.getMouseListeners();
savedMouseMotionListeners = explorerTree.getMouseMotionListeners();
for (final MouseListener listener : savedMouseListeners) {
explorerTree.removeMouseListener(listener);
}
for (final MouseMotionListener listener : savedMouseMotionListeners) {
explorerTree.removeMouseMotionListener(listener);
}
}
}
/**
* Returns the current status of the explorer
* @return boolean
*/
public boolean isMouseEnabled() {
return mouseInputEnabled;
}
/**
* Selects the correponding gem in the explorer. This allows for parallel selection between the TableTop and the explorer.
* @param gem
*/
public void selectGem (Gem gem) {
if (gemToNodesMap != null && gem != null) {
DefaultMutableTreeNode node = gemToNodesMap.get(gem);
if (node != null) {
TreePath path = new TreePath(node.getPath());
getExplorerTree().setSelectionPath(path);
}
} else {
getExplorerTree().setSelectionPath(null);
}
}
/**
* Selects the correponding gem in the explorer. This allows for parallel selection between the TableTop and the explorer.
* @param input
*/
public void selectInput (Gem.PartInput input) {
if (inputsToNodeMap != null && input != null) {
DefaultMutableTreeNode node = inputsToNodeMap.get(input);
if (node != null) {
TreePath path = new TreePath(node.getPath());
getExplorerTree().setSelectionPath(path);
}
}
}
/**
* Selects the correponding gem in the explorer. This allows for parallel selection between the TableTop and the explorer.
*/
public void selectRoot () {
TreePath path = new TreePath(explorerTreeRoot.getPath());
getExplorerTree().setSelectionPath(path);
}
/**
* Display the gem name editor associated with the defined gem
* @param gem
*/
public void renameGem(Gem gem) {
ExplorerGemNode node = gemToNodesMap.get(gem);
explorerTree.startEditingAtPath(new TreePath(node.getPath()));
}
/**
* Edit the ValueGem specified in the parameter
* @param valueGem
*/
public void editValueGem(ValueGem valueGem) {
ExplorerGemNode node = gemToNodesMap.get(valueGem);
explorerTree.startEditingAtPath(new TreePath(node.getPath()));
}
/**
* Edit the Gem.PartInput specified in the parameters
* @param partInput
*/
public void editPartInput(Gem.PartInput partInput) {
if (!explorerOwner.canEditInputsAsValues()) {
throw new IllegalStateException("Editing PartInput is not supported");
}
ExplorerInputNode node = inputsToNodeMap.get(partInput);
explorerTree.startEditingAtPath(new TreePath(node.getPath()));
}
/**
* This returns the old selection path, if there is one. This is useful for determining
* if the user clicked on a part-input and the selection immediately changes to the input's
* gem because that gem became selected on the table top.
* @return the old selection path for the explorer tree
*/
public final TreePath getOldSelectionPath() {
return oldSelectionPath;
}
}