/* * 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. */ /* * BrowserTree.java * Creation date: (10/29/00 11:16:35 AM) * By: Luke Evans */ package org.openquark.gems.client.browser; import java.awt.AlphaComposite; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; import java.awt.KeyboardFocusManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Vector; import javax.swing.JLabel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import org.openquark.cal.compiler.ModuleName; import org.openquark.cal.compiler.QualifiedName; import org.openquark.cal.compiler.ScopedEntityNamingPolicy; import org.openquark.cal.compiler.ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous; import org.openquark.cal.services.GemEntity; import org.openquark.cal.services.MetaModule; import org.openquark.gems.client.GemEntitySelection; import org.openquark.gems.client.PickListKeyStrokeNavigator; import org.openquark.gems.client.ToolTipHelpers; import org.openquark.gems.client.navigator.NavAddress; import org.openquark.gems.client.navigator.NavFrameOwner; import org.openquark.gems.client.utilities.MouseClickDragAdapter; import org.openquark.util.UnsafeCast; import org.openquark.util.ui.UtilTree; /** * Extension of JTree adding drag source features. * Also added are pop up menus which enable sorting capabilities. * @author Luke Evans */ public class BrowserTree extends UtilTree { private static final long serialVersionUID = -283997771683405356L; /** * todoSN - A JDK 1.4 problem (see java bug #4485987, 4669873) periodically freezes * the drag image and makes it unusable. The problem seems to be related to the * calls to paintImmediately() and drawImage() in the drop target version of dragOver() * in the DnD handler (both here in the BrowserTree and in the TableTop). * The drag image can be enabled/disabled by setting this flag so testing to see * if the problem is fixed, supposedly for 1.4.1, should be trivial. * * A boolean to turn drag images on/off when dragging from the gem browser. Enabling the drag image * will disable the drag cursor, while disabling the drag image will enable the drag cursor. */ private static final boolean DRAG_IMAGE_ENABLED = false; /** The display name given to modules with "" (no name) as their name. */ public static final String DEFAULT_MODULE_NAME = "<default>"; /** Whether the system supports drag images natively (otherwise we must render our own). */ private static final boolean DRAG_IMAGE_SUPPORTED = DragSource.isDragImageSupported(); /** The handler for drag and drop operations. */ private final DragAndDropHandler dragAndDropHandler; /** Rendering hints - render at highest quality*/ private static final RenderingHints RENDERING_HINTS_HIGH_QUALITY = new RenderingHints(null); /** Used by saveState() and restoreSavedState(). * The saved selected node and its siblings, in the order that they appear as children of the parent. * If the selected node is not displayed after the model is reloaded this list will be used to choose * the closest(/best) one which does. */ private List<Object> savedChildrenList; static { RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); RENDERING_HINTS_HIGH_QUALITY.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } /** The BrowserTreeActions object associated with this tree. */ private final BrowserTreeActions browserTreeActions = new BrowserTreeActions(this); /** The navigator owner that is used for editing/viewing metadata. */ private NavFrameOwner navigatorOwner; /** The popup menu provider for this tree. */ private PopupMenuProvider popupMenuProvider; /** Flag to indicate whether or not the Gems in the Tree are draggable.*/ private boolean isDraggable; /** Flag to indicate whether or not type expressions should be displayed in the tree. */ private boolean displayTypeExpr; /** * Whether the current module should be highlighted in a different colour. * Note: Should this flag be in the cell renderer instead? */ private boolean highlightCurrentModule; /** A list to hold any LeafNodeTriggeredListeners that are interested in receiving events for this BrowserTree * Use a synchronized list, as the collection may be added-to by different threads..*/ private final List<LeafNodeTriggeredListener> leafNodeTriggeredListeners = new Vector<LeafNodeTriggeredListener>(); /** * Handler for drag and drop events. * Creation date: (03/15/2002 2:03:35 PM) * @author Edward Lam */ private class DragAndDropHandler implements DragGestureListener, DragSourceListener, DropTargetListener { /** The image that shows up when dragging. */ private BufferedImage dragImage = null; /** The offset of the mouse from the image origin while dragging. */ private final Point mousePointOffset = new Point(0, 0); /** The bounds of the last drawn drag ghost. */ private final Rectangle lastGhostRect = new Rectangle(); /** The base image (a black arrow) used to build images for cursors. */ private Image baseCursorImage = null; /** The drag and drop cursor to use when over a valid drop target. */ private Cursor dndValidCursor = null; /** * Constructor for a drag-and-drop handler * Creation date: (03/15/2002 2:10:00 PM) */ private DragAndDropHandler() { // create the drag gesture recognizer for this tree. DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(BrowserTree.this, DnDConstants.ACTION_COPY_OR_MOVE, this); // Load the image to use as a base when building cursor images baseCursorImage = new javax.swing.ImageIcon(BrowserTree.class.getResource("/Resources/arrowMed.gif")).getImage(); } /* * Methods implementing DragGestureListener *********************************************************** */ /** * A <code>DragGestureRecognizer</code> has detected a platform-dependent drag initiating gesture and * is notifying this listener in order for it to initiate the action for the user. * <P> * @param dge the <code>DragGestureEvent</code> describing the gesture that has just occurred */ public void dragGestureRecognized(DragGestureEvent dge) { // First, check to make sure that dragging is enabled. if (!isDraggable) { // Do nothing. return; } // Check for valid selections to drag if (getSelectionCount() == 0) { // Nothing selected, no drag possible return; } // Dragging of an individual drawer is allowed; check if this is the case if (getSelectionCount() == 1) { TreePath[] selectedPaths = getSelectionPaths(); TreeNode endpoint = (TreeNode)selectedPaths[0].getLastPathComponent(); if (endpoint instanceof MaterialGemDrawer) { // Get a cursor based on the selected tree nodes dndValidCursor = buildValidDnDCursor(selectedPaths); if (!DRAG_IMAGE_ENABLED) { // Someone has disabled the drag image feature so start the drag without it. dge.startDrag(null, null, null, new GemDrawerSelection(((MaterialGemDrawer)endpoint).getModuleName()), this); } else { Point ptDragOrigin = dge.getDragOrigin(); TreePath path = getPathForLocation(ptDragOrigin.x, ptDragOrigin.y); if (path == null) { // not a drag condition return; } Rectangle pathBounds = getPathBounds(path); mousePointOffset.setLocation(ptDragOrigin.x - pathBounds.x, ptDragOrigin.y - pathBounds.y); // Start the drag. Prepare the drag image, in case the system can use it. prepareDragImage(path); dge.startDrag(null, dragImage, mousePointOffset, new GemDrawerSelection(((MaterialGemDrawer)endpoint).getModuleName()), this); } return; } } // Make the list we'll build the list of gems in List<GemEntity> selectedGems = new ArrayList<GemEntity>(); // Get all the selected paths TreePath[] paths = getSelectionPaths(); // For each path, check it's a supercombinator and add to the string // Note: (for now) we don't add sc's that aren't visible.. for (final TreePath treePath : paths) { // Get the fully qualified name of the supercombinator represented by this path GemEntity gemEntity = getEntityFromPath(treePath); // If this is non-null, emit the name if (gemEntity != null && ((BrowserTreeModel)getModel()).isVisibleGem(gemEntity)) { // Add the supercombinator selectedGems.add(gemEntity); } } // Did we get any gems to drag? if (!selectedGems.isEmpty()) { // Build a selection list comprising of all the gem names to be transfered GemEntitySelection scs = new GemEntitySelection(selectedGems); // Get a cursor based on the selected tree nodes dndValidCursor = buildValidDnDCursor(paths); // Make sure that the drag image has been enabled if (!DRAG_IMAGE_ENABLED) { // Someone has disabled the drag image feature so start the drag without it. dge.startDrag(null, null, null, scs, this); } else { // Start the drag gesture // figure out the offset of the mouse from the bounding rectangle of the cell being dragged Point ptDragOrigin = dge.getDragOrigin(); TreePath path = getPathForLocation(ptDragOrigin.x, ptDragOrigin.y); if (path == null || !Arrays.asList(paths).contains(path)) { // not a drag condition return; } Rectangle pathBounds = getPathBounds(path); mousePointOffset.setLocation(ptDragOrigin.x - pathBounds.x, ptDragOrigin.y - pathBounds.y); // prepare the image prepareDragImage(path); // Start the drag. Give the drag image, in case the system can use it. dge.startDrag(null, dragImage, mousePointOffset, scs, this); } } } /** * Renders the mouse drag image for the specific tree path. */ private void prepareDragImage(TreePath path) { // First calculate an image to drag (maybe later the gem drag ghost?) // Get the tree cell renderer Rectangle pathBounds = getPathBounds(path); JLabel rendererComponent = (JLabel) getCellRenderer().getTreeCellRendererComponent ( BrowserTree.this, // tree path.getLastPathComponent(), // value false, // isSelected isExpanded(path), // isExpanded getModel().isLeaf(path.getLastPathComponent()), // isLeaf 0, // row false // hasFocus ); // The layout manager normally does this... rendererComponent.setSize((int)pathBounds.getWidth(), (int)pathBounds.getHeight()); // Get a buffered image of the selection for dragging a ghost image dragImage = new BufferedImage( (int)pathBounds.getWidth(), (int)pathBounds.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE ); // Get a graphics context for this image, suggest antialiasing. Graphics2D g2d = dragImage.createGraphics(); // Might as well render at the highest quality, if we're only going to do this once g2d.setRenderingHints(RENDERING_HINTS_HIGH_QUALITY); // Make the image ghostlike g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.5f)); // Ask the cell renderer to paint itself into the BufferedImage rendererComponent.paint(g2d); // Finished with the graphics context now g2d.dispose(); } /** * Builds a cursor for a drag and drop that can be used over a valid drop target. * Creation date: (06/12/2002 11:15:00 AM). * @param paths javax.swing.tree.TreePath[] the paths of the selected tree nodes. * @return Cursor */ private Cursor buildValidDnDCursor(TreePath[] paths) { // Get the toolkit and scale the composite image to an appropriate size. Toolkit toolkit = Toolkit.getDefaultToolkit(); Dimension bestsize = toolkit.getBestCursorSize(1,1); if (bestsize.width <= 1) { // We don't appear to support custom cursors, so just use a cross hair return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); } // We're looking to see if more than one type of node was selected (constructor vs. sc) and // which came first. The array holds the indices of the first occurrence of each type and the boolean // tracks which type came first. boolean isFirstNodeConstructor = false; int gemCount = 0; int[] nodes = {-1, -1}; // Go thru the paths and see which nodes were dragged for (int i = 0; i < paths.length && nodes[1] == -1; i++) { // Get the entity represented by this path GemEntity gemEntity = getEntityFromPath(paths[i]); // If the sc is non-null see if it is a data constructor or just a regular sc. if (gemEntity != null) { boolean isConstructor = gemEntity.isDataConstructor(); gemCount++; if (nodes[0] == -1) { // We have found our first node so remember the details nodes[0] = i; isFirstNodeConstructor = isConstructor; } else if (isConstructor != isFirstNodeConstructor) { // The first node has already been found and the second node is a different type. nodes[1] = i; } } } // Don't build a cursor if we have no gems to drag if (gemCount == 0) { return null; } // If gem count is greater than 1 but we only have one type, just display the same type twice if (gemCount > 1 && nodes[1] == -1) { nodes[1] = nodes[0]; } // Get the icons used in the labels and track the size of the composite image we need to build javax.swing.Icon[] icons = new javax.swing.Icon[2]; int compWidth = baseCursorImage.getWidth(null); int compHeight = baseCursorImage.getHeight(null); for (int i = 0; i < nodes.length && nodes[i] >= 0; i++) { javax.swing.JLabel label = (javax.swing.JLabel)getCellRenderer().getTreeCellRendererComponent ( BrowserTree.this, // tree paths[nodes[i]].getLastPathComponent(), // value false, // isSelected isExpanded(paths[nodes[i]]), // isExpanded getModel().isLeaf(paths[nodes[i]].getLastPathComponent()), // isLeaf 0, // row false // hasFocus ); icons[i] = label.getIcon(); // Start tracking the size of the composite cursor image compWidth += icons[i].getIconWidth(); compHeight += icons[i].getIconHeight(); } // Get a buffered image (and its graphics) to build a composite cursor image BufferedImage compImage = new BufferedImage(compWidth, compHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = (Graphics2D)compImage.getGraphics(); // Paint the base image onto the composite graphics g2d.drawImage(baseCursorImage, 0, 0, null); // Paint the icons below and to the right of the base image. The -2's are used to reduce the amount // of space between the bottom of the arrow and the top of the first tree node image. int locX = baseCursorImage.getWidth(null)-2; int locY = baseCursorImage.getHeight(null)-2; for (int i = 0; i < icons.length && nodes[i] >=0; i++) { icons[i].paintIcon(null, g2d, locX, locY); // Shift the location so the next icon image is painted a little to the right. locX += 8; } g2d.dispose(); // Scale the image correctly BufferedImage scaledCompImage = new BufferedImage(bestsize.width, bestsize.height, BufferedImage.TYPE_INT_ARGB); Graphics2D scaledGraphics = scaledCompImage.createGraphics(); scaledGraphics.drawImage(compImage, null, null); scaledGraphics.dispose(); // Actually build a cursor from the scaled image return toolkit.createCustomCursor(scaledCompImage, new Point(0, 0), "dndValidCursor"); } /* * Methods implementing DragSourceListener ************************************************************ */ /** * This method is invoked to signify that the Drag and Drop operation is complete. * The getDropSuccess() method of the <code>DragSourceDropEvent</code> can be used to * determine the termination state. The getDropAction() method returns the operation * that the <code>DropTarget</code> selected (via the DropTargetDropEvent acceptDrop() parameter) * to apply to the Drop operation. Once this method is complete, the current * <code>DragSourceContext</code> and associated resources become invalid. * <P> * @param dsde the <code>DragSourceDropEvent</code> */ public void dragDropEnd(DragSourceDropEvent dsde) { // If necessary, erase the last ghost image if (!DRAG_IMAGE_SUPPORTED) { paintImmediately(lastGhostRect.getBounds()); } // set image to null - we use this to find out when the operation ended dragImage = null; } /** * Called as the hotspot enters a platform dependent drop site. * This method is invoked when the following conditions are true: * <UL> * <LI>The logical cursor's hotspot initially intersects a GUI <code>Component</code>'s visible geometry. * <LI>That <code>Component</code> has an active <code>DropTarget</code> associated with it. * <LI>The <code>DropTarget</code>'s registered <code>DropTargetListener</code> dragEnter() method * is invoked and returns successfully. * <LI>The registered <code>DropTargetListener</code> invokes the <code>DropTargetDragEvent</code>'s * acceptDrag() method to accept the drag based upon interrogation of the source's * potential drop action(s) and available data types (<code>DataFlavor</code>s). * </UL> *<P> *@param dsde the <code>DragSourceDragEvent</code> */ public void dragEnter(DragSourceDragEvent dsde) { if (!DRAG_IMAGE_ENABLED) { // Set the cursor to the valid DnD cursor since we are entering a valid drop site. dsde.getDragSourceContext().setCursor(dndValidCursor); } } /** * Called as the hotspot exits a platform dependent drop site. * This method is invoked when the following conditions * are true: * <UL> * <LI>The cursor's logical hotspot no longer intersects the visible geometry of the <code>Component</code> * associated with the previous dragEnter() invocation. * </UL> * OR * <UL> * <LI>The <code>Component</code> that the logical cursor's hotspot intersected that resulted in the * previous dragEnter() invocation no longer has an active <code>DropTarget</code> or * <code>DropTargetListener</code> associated with it. * </UL> * OR * <UL> * <LI> The current <code>DropTarget</code>'s <code>DropTargetListener</code> has invoked rejectDrag() * since the last dragEnter() or dragOver() invocation. * </UL> * <P> * @param dse the <code>DragSourceEvent</code> */ public void dragExit(DragSourceEvent dse) { if (!DRAG_IMAGE_ENABLED) { // Set the invalid cursor since we are leaving a valid drop site. dse.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop); } } /** * Called as the hotspot moves over a platform dependent drop site. * This method is invoked when the following conditions * are true: *<UL> *<LI>The cursor's logical hotspot has moved but still * intersects the visible geometry of the <code>Component</code> * associated with the previous dragEnter() invocation. * <LI>That <code>Component</code> still has a * <code>DropTarget</code> associated with it. * <LI>That <code>DropTarget</code> is still active. * <LI>The <code>DropTarget</code>'s registered * <code>DropTargetListener</code> dragOver() method * is invoked and returns successfully. * <LI>The <code>DropTarget</code> does not reject * the drag via rejectDrag() * </UL> * <P> * @param dsde the <code>DragSourceDragEvent</code> */ public void dragOver(DragSourceDragEvent dsde) { if (!DRAG_IMAGE_ENABLED) { // Set the invalid cursor since we are leaving a valid drop site. dsde.getDragSourceContext().setCursor(dndValidCursor); } } /** * Called when the user has modified the drop gesture. * This method is invoked when the state of the input * device(s) that the user is interacting with changes. * Such devices are typically the mouse buttons or keyboard * modifiers that the user is interacting with. * <P> * @param dsde the <code>DragSourceDragEvent</code> */ public void dropActionChanged(DragSourceDragEvent dsde) { } /* * Methods implementing DropTargetListener ************************************************************ */ /** * Called when a drag operation has encountered the <code>DropTarget</code>. * <P> * Creation date: (03/14/2002 5:59:00 PM) * @param dtde the <code>DropTargetDragEvent</code> */ public void dragEnter(DropTargetDragEvent dtde){ } /** * Called when a drag operation is ongoing on the <code>DropTarget</code>. * <P> * Creation date: (03/14/2002 5:59:00 PM) * @param dtde the <code>DropTargetDragEvent</code> */ public void dragOver(DropTargetDragEvent dtde){ // To draw the drag image: // First, repaint the real estate the drag image last occupied. // Note that simply calling repaint() won't work because it effectively delays the repainting, // possibly until after you have drawn the new drag image, and therefore, erases all or part of it. // You really must paint the area immediately, using the, you guessed it, paintImmediately() method. // // Second, you draw the ghost image in its new location. // Note that you draw the image the same distance away from the mouse pointer as when the node // was first clicked. if (!DRAG_IMAGE_SUPPORTED && dragImage != null) { Graphics2D g2d = (Graphics2D)getGraphics(); // Calculate new location Point mouseLocation = dtde.getLocation(); int newX = mouseLocation.x - mousePointOffset.x; int newY = mouseLocation.y - mousePointOffset.y; // Update if the location changed if (newX != lastGhostRect.x || newY != lastGhostRect.y) { // Erase the last ghost image and cue line paintImmediately(lastGhostRect.getBounds()); // Remember where you are about to draw the new ghost image lastGhostRect.setBounds(newX, newY, dragImage.getWidth(), dragImage.getHeight()); // Draw the ghost image g2d.drawImage(dragImage, AffineTransform.getTranslateInstance(lastGhostRect.getX(), lastGhostRect.getY()), null); } g2d.dispose(); } } /** * Called if the user has modified the current drop gesture. * <P> * Creation date: (03/14/2002 5:59:00 PM) * @param dtde the <code>DropTargetDragEvent</code> */ public void dropActionChanged(DropTargetDragEvent dtde){ } /** * The drag operation has departed the <code>DropTarget</code> without dropping. * <P> * Creation date: (03/14/2002 5:59:00 PM) * @param dte the <code>DropTargetEvent</code> */ public void dragExit(DropTargetEvent dte){ // If necessary, erase the last ghost image if (!DRAG_IMAGE_SUPPORTED) { paintImmediately(lastGhostRect.getBounds()); } } /** * The drag operation has terminated with a drop on this <code>DropTarget</code>. * This method is responsible for undertaking the transfer of the data associated with the * gesture. The <code>DropTargetDropEvent</code> provides a means to obtain a <code>Transferable</code> * object that represents the data object(s) to be transfered.<P> * From this method, the <code>DropTargetListener</code> shall accept or reject the drop via the * acceptDrop(int dropAction) or rejectDrop() methods of the <code>DropTargetDropEvent</code> parameter. * <P> * Subsequent to acceptDrop(), but not before, <code>DropTargetDropEvent</code>'s getTransferable() * method may be invoked, and data transfer may be performed via the returned <code>Transferable</code>'s * getTransferData() method. * <P> * At the completion of a drop, an implementation of this method is required to signal the success/failure * of the drop by passing an appropriate <code>boolean</code> to the <code>DropTargetDropEvent</code>'s * dropComplete(boolean success) method. * <P> * Note: The actual processing of the data transfer is not required to finish before this method returns. It may be * deferred until later. * <P> * Creation date: (03/14/2002 5:59:00 PM) * @param dtde the <code>DropTargetDropEvent</code> */ public void drop(DropTargetDropEvent dtde){ dtde.rejectDrop(); } } /** * Listens for the user triggering the pop up menu mouse event. * Note: This listener automatically corrects for the strange * bug that right clicking on a JTree node does not select it. */ private class PopupListener extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { // Correct for the RMB not selecting bug. if (javax.swing.SwingUtilities.isRightMouseButton(e)) { int selectedRow = getRowForLocation(e.getX(), e.getY()); // If there is actually a row there, then select it. // Else, do nothing. if (selectedRow != -1) { setSelectionRow(selectedRow); } } maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } /** * Show the popup, if the given mouse event is the popup trigger. * @param e the mouse event. */ private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger() && popupMenuProvider != null) { // Get the popup menu for the current selection path. TreePath selectionPath = getSelectionPath(); JPopupMenu browserTreePopupMenu = popupMenuProvider.getPopupMenu(selectionPath); // Show the popup menu if it has at least one item. if (browserTreePopupMenu.getSubElements().length > 0) { browserTreePopupMenu.show(e.getComponent(), e.getX(), e.getY()); } } } } /** * Interface to define a provider for a popup menu for this tree. * @author Edward Lam */ public interface PopupMenuProvider { /** * Get the popup menu for a given selection path in this tree. * @param selectionPath the path on which the popup menu was invoked. * @return the popup menu for this tree. */ JPopupMenu getPopupMenu(TreePath selectionPath); } /** * This listener fires a leafNodeTriggered event if the user selects leaf nodes and hits enter. * @author Edward Lam */ private class KeyStrokeNavigator extends PickListKeyStrokeNavigator { /** * Constructor for the KeyStrokeNavigator class. * Creation date: (07/09/2002 8:33:00 AM). * @param component Component - the component that this listener is navigating. */ public KeyStrokeNavigator(Component component) { super(component); } /** * Watch for the user to press ENTER. * Creation date: (04/16/2002 3:33:00 PM) */ @Override public void keyPressed(KeyEvent evt) { if (evt.getKeyCode() == KeyEvent.VK_ENTER) { // Get the currently selected paths TreePath[] selectedPaths = getSelectionPaths(); // Loop thru the paths and build a list of objects for all the leaves if (selectedPaths != null) { List<Object> userObjects = new ArrayList<Object>(); for (final TreePath treePath : selectedPaths) { Object lastComponent = treePath.getLastPathComponent(); if (lastComponent instanceof BrowserTreeNode && ((BrowserTreeNode)lastComponent).isLeaf()) { userObjects.add(((BrowserTreeNode)lastComponent).getUserObject()); } } // Fire off an event for these nodes fireLeafNodeTriggeredEvent(userObjects); } } // Hand the event off to the super version super.keyPressed(evt); } /** * Get the String text displayed in the nth row of the pick list. * @param index the index of the string to return * @return the text displayed in the nth row */ @Override public String getNthRowString(int index){ // get the node corresponding to this row TreePath tp = getPathForRow(index); BrowserTreeNode node = (BrowserTreeNode) tp.getLastPathComponent(); // use the cell renderer to find out what text it would print out boolean isLeaf = (node instanceof GemTreeNode); TreeCellRenderer tcr = getCellRenderer(); Component c = tcr.getTreeCellRendererComponent(BrowserTree.this, node, false, false, isLeaf, 0, false); // The returned component is an instance of JLabel. Get the text which would be displayed, in lower case. String rowString = ((JLabel) c).getText(); return rowString; } /** * Get the number of rows in the pick list. * Creation date: (11/09/2001 3:03:52 PM) * @return int the number of rows in the pick list */ @Override public int getNumRows(){ return getRowCount(); } /** * Get the index of the row in the pick list which is currently selected. * Creation date: (11/09/2001 3:03:52 PM) * @return int the index of the currently selected row */ @Override public int getSelectedRowIndex(){ return getLeadSelectionRow(); } /** * Move the selection in the pick list to a given row. * Creation date: (11/09/2001 3:03:52 PM) * @param index int the index of the row to set selected */ @Override public void selectRow(int index){ setSelectionRow(index); scrollRowToVisible(index); } } /** * This listener fires a leafNodeTriggered event if the user double-clicks on a leaf node. * @author Frank Worsley */ private class LeafNodeMouseListener extends MouseClickDragAdapter { @Override public boolean mouseReallyClicked(MouseEvent e) { boolean doubleClicked = super.mouseReallyClicked(e); TreePath[] selectedPaths = getSelectionPaths(); // If there are no selected paths, then use the path that was clicked on. if (selectedPaths == null || selectedPaths.length == 0) { selectedPaths = new TreePath[1]; selectedPaths[0] = getPathForLocation(e.getX(), e.getY()); } if (doubleClicked && SwingUtilities.isLeftMouseButton(e)) { List<Object> userObjects = new ArrayList<Object>(); for (final TreePath treePath : selectedPaths) { if (treePath == null) { continue; } Object lastComponent = treePath.getLastPathComponent(); if (lastComponent instanceof BrowserTreeNode && ((BrowserTreeNode) lastComponent).isLeaf()) { userObjects.add(((BrowserTreeNode) lastComponent).getUserObject()); } } if (userObjects.size() > 0) { fireLeafNodeTriggeredEvent(userObjects); } } return doubleClicked; } } /** * Default BrowserTree constructor. */ public BrowserTree() { // Use an empty model until the Services get initialized BrowserTreeModel browserTreeModel = new BrowserTreeModel(); setModel(browserTreeModel); // We don't want to see the root node. setRootVisible(false); setShowsRootHandles(true); // Set the renderer for this tree setCellRenderer(new BrowserTreeCellRenderer()); // Add pop up menu capabilities. addMouseListener(new PopupListener()); // Add the ability to quickly get to a gem by typing in its name KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyStrokeNavigator(this)); // Add a listener to fire leaf node events if the user double-clicks addMouseListener(new LeafNodeMouseListener()); // Register the drag-and-drop handler dragAndDropHandler = new DragAndDropHandler(); // Lie and say we are a drop target. // We do this so we can draw the drag image if that operation is not // supported natively on the platform. We never actually accept any drops. setDropTarget(new DropTarget(this, dragAndDropHandler)); // Set the default popup menu provider popupMenuProvider = new PopupMenuProvider() { public JPopupMenu getPopupMenu(TreePath selectionPath) { return browserTreeActions.getDefaultBrowserTreePopup(selectionPath); } }; isDraggable = true; highlightCurrentModule = true; // Add a listener that will update the metadata viewer when the selection changes. addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent evt) { TreePath path = evt.getNewLeadSelectionPath(); if (path == null) { return; } BrowserTreeNode node = (BrowserTreeNode) path.getLastPathComponent(); if (navigatorOwner != null) { final NavAddress address; if (node instanceof GemTreeNode) { GemEntity gemEntity = (GemEntity) node.getUserObject(); address = NavAddress.getAddress(gemEntity); navigatorOwner.displayMetadata(address, false); } else if (node instanceof GemDrawer) { final GemDrawer gemDrawer = (GemDrawer) node; final ModuleName moduleName = gemDrawer.getModuleName(); if (gemDrawer.isNamespaceNode()) { address = NavAddress.getModuleNamespaceAddress(moduleName); navigatorOwner.displayMetadata(address, false); } else { MetaModule metaModule = navigatorOwner.getPerspective().getMetaModule(moduleName); if (metaModule != null) { address = NavAddress.getAddress(metaModule); navigatorOwner.displayMetadata(address, false); } } } } } }); } /** * Set the navigator owner to use for editing/displaying gem metadata. * @param navigatorOwner the navigator owner to use */ public void setNavigatorOwner(NavFrameOwner navigatorOwner) { this.navigatorOwner = navigatorOwner; } /** * @return the navigator owner being used to display/edit metadata. This may be * null if no navigator owner has been setup. */ public NavFrameOwner getNavigatorOwner() { return navigatorOwner; } /** * Expand all visible tree nodes at a given depth. * <p> * <strong>This should be run on the AWT event-handler thread.</strong> * * @param depth 0-based level of the nodes to expand. Nodes with this depth will be expanded. */ public void expandLevel(int depth) { for (Enumeration<BrowserTreeNode> nodeEnum = UnsafeCast.unsafeCast(((BrowserTreeNode)getModel().getRoot()).breadthFirstEnumeration()); nodeEnum.hasMoreElements(); ) { BrowserTreeNode node = nodeEnum.nextElement(); int nodeDepth = node.getLevel(); if (nodeDepth <= depth) { expandPath(new TreePath(node.getPath())); } else if (nodeDepth > depth) { return; // breadth first, so all leftover elements will have depth >= the current depth } } } /** * If there is a node in the tree for the given entity, this will select that * node and scroll it into view. If there is no such node, then this does nothing. * <p> * <strong>This should be run on the AWT event-handler thread.</strong> * * @param newGemEntity the entity to select */ public void selectGemNode(GemEntity newGemEntity) { BrowserTreeModel browserTreeModel = (BrowserTreeModel) getModel(); BrowserTreeNode gemNode = browserTreeModel.getTreeNode(newGemEntity); if (gemNode != null) { TreePath nodePath = new TreePath(gemNode.getPath()); scrollPathIntoView(nodePath); setSelectionPath(nodePath); } } /** * If there is a drawer node with given name, this will select that * node and scroll it into view. If there is no such node, then this does nothing. * <p> * <strong>This should be run on the AWT event-handler thread.</strong> * * @param drawerName the name of the drawer to select */ public void selectDrawerNode(ModuleName drawerName) { Enumeration<BrowserTreeNode> allNodes = UnsafeCast.unsafeCast(((BrowserTreeNode)getModel().getRoot()).breadthFirstEnumeration()); while (allNodes.hasMoreElements()) { BrowserTreeNode node = allNodes.nextElement(); if (node instanceof GemDrawer && ((GemDrawer)node).getModuleName().equals(drawerName)) { TreePath nodePath = new TreePath(node.getPath()); setSelectionPath(nodePath); scrollPathIntoView(nodePath); break; } } } /** * If there is a node with the given title, this will select that node * and scroll it into view. If there is no such node, then this does nothing. * If there are several nodes with the given title, the first node will be selected. * <p> * <strong>This should be run on the AWT event-handler thread.</strong> * * @param nodeTitle the title of the node to select */ public void selectNode(String nodeTitle) { Enumeration<BrowserTreeNode> allNodes = UnsafeCast.unsafeCast(((BrowserTreeNode)getModel().getRoot()).breadthFirstEnumeration()); while (allNodes.hasMoreElements()) { BrowserTreeNode node = allNodes.nextElement(); if (node.getDisplayedString().equals(nodeTitle)) { TreePath nodePath = new TreePath(node.getPath()); setSelectionPath(nodePath); scrollPathIntoView(nodePath); break; } } } /** * {@inheritDoc} * Additionally saves the list of children that includes the selected node. */ @Override public void saveState() { // Remember the currently expanded nodes. Enumeration<TreePath> expandedPaths = getExpandedDescendants(new TreePath(getModel().getRoot())); savedExpandedPaths = new HashSet<TreePath>(); if (expandedPaths != null) { while (expandedPaths.hasMoreElements()) { TreePath path = expandedPaths.nextElement(); savedExpandedPaths.add(path); } } // Remember the selected node and the current position of the selected node in respect to all its siblings savedSelectionPath = getSelectionPath(); if (savedSelectionPath != null) { BrowserTreeNode parentNode = (BrowserTreeNode) savedSelectionPath.getParentPath().getLastPathComponent(); Enumeration<TreeNode> enumChildren = UnsafeCast.unsafeCast(parentNode.children()); savedChildrenList = new ArrayList<Object>(); while (enumChildren.hasMoreElements()) { savedChildrenList.add(enumChildren.nextElement()); } } } /** * {@inheritDoc} */ @Override public void restoreSavedState() { for (final TreePath treePath : savedExpandedPaths) { this.expandPath(treePath); } if (savedSelectionPath != null) { TreeNode lastPathComponent = ((TreeNode) savedSelectionPath.getLastPathComponent()); TreePath parentPath = savedSelectionPath.getParentPath(); TreeNode parentNode = (TreeNode)parentPath.getLastPathComponent(); TreePath newPath = parentPath; // starts out as parent path, but can be changed below. // Check if the path contains a drawerNode and is still valid (ie. the module is not removed) boolean workingModuleExists = false; Object[] nodesOnPath = savedSelectionPath.getPath(); int indexOfDrawerNodeOnPath = 0; for (int i = 0; i < savedSelectionPath.getPathCount(); i++) { if (nodesOnPath[i] instanceof GemDrawer) { indexOfDrawerNodeOnPath = i; TreeNode prevNodeOnPath = (TreeNode) nodesOnPath[i - 1]; for (int j = 0; j < prevNodeOnPath.getChildCount(); j++) { if (prevNodeOnPath.getChildAt(j).equals(nodesOnPath[i])) { workingModuleExists = true; break; } } break; } } // If there was a module on the path and it no longer exists, its parent will be selected if (indexOfDrawerNodeOnPath != 0 && !workingModuleExists) { TreeNode[] tempNewPath = new TreeNode[indexOfDrawerNodeOnPath]; for (int i = 0; i < indexOfDrawerNodeOnPath; i++){ tempNewPath[i]= (TreeNode) nodesOnPath[i]; } newPath = new TreePath(tempNewPath); // Leaf nodes are reconstructed therefore need to compare the user objects to find the correct path. } else if (lastPathComponent instanceof GemTreeNode) { GemEntity lastNodeObj = (GemEntity)((GemTreeNode) lastPathComponent).getUserObject(); if (!((BrowserTreeModel)getModel()).getShowPublicGemsOnly() || lastNodeObj.getScope().isPublic()){ // Finding the new tree node for the gem entity GemTreeNode child = getNodeFromEntity(parentNode, lastNodeObj); if (child != null){ // for the case of a search node, the child can become null newPath = new TreePath(child.getPath()); } } else { // If only showing public gems and the selected gem is not public, propagate selection to one of its siblings. int prevChildIndex = savedChildrenList.indexOf(lastPathComponent); int prevChildCount = savedChildrenList.size(); // First search downward from the index of the selected gem for the next public gem for (int i = prevChildIndex; i < prevChildCount; i++) { TreeNode oldSibling = (TreeNode) savedChildrenList.get(i); if (oldSibling instanceof GemTreeNode) { TreePath publicChildPath = getPublicGemTreeNodePath(parentNode, (GemTreeNode) oldSibling); if (publicChildPath != null) { newPath = publicChildPath; break; } } } // If cannot find any public gems so far, go back to the index and search upward if (newPath == parentPath) { for (int i = prevChildIndex - 1; i >= 0; i--) { TreeNode oldSibling = (TreeNode) savedChildrenList.get(i); if (oldSibling instanceof GemTreeNode) { TreePath publicChildPath = getPublicGemTreeNodePath(parentNode, (GemTreeNode)oldSibling); if (publicChildPath != null) { newPath = publicChildPath; break; } } } } } } else { newPath = savedSelectionPath; } // If the last component on the new path is an empty category node, propagate the selection up the hierarchy TreeNode newLastPathComponent = (TreeNode)newPath.getLastPathComponent(); if (newLastPathComponent instanceof GemCategoryNode) { while(newLastPathComponent.getChildCount() == 0 && newLastPathComponent instanceof GemCategoryNode) { newPath = newPath.getParentPath(); newLastPathComponent = (TreeNode)newPath.getLastPathComponent(); } } setSelectionPath(newPath); scrollPathIntoView(newPath); } } /** * Get the TreeNodePath to the child if the child's gem entity is public. * Otherwise return null. * @param parentNode the parent tree node. * @param oldGemTreeNode the gem tree node to look at (from the saved state) * @return the child if the child's gem entity is public. * Otherwise null. */ private TreePath getPublicGemTreeNodePath(TreeNode parentNode, GemTreeNode oldGemTreeNode) { GemEntity childUserObject = (GemEntity)oldGemTreeNode.getUserObject(); if (childUserObject.getScope().isPublic()) { GemTreeNode sibling = getNodeFromEntity(parentNode, childUserObject); return new TreePath(sibling.getPath()); } return null; } /** * To find the tree node that contains the gem entity * * @param parentNode the parent of the node of interest * @param gemEntity the entity that we wish to find * @return child the GemTreeNode that contains the entity */ private GemTreeNode getNodeFromEntity(TreeNode parentNode, GemEntity gemEntity){ // Need to compare the qualified name because it is consistent even after recompiling QualifiedName entityName = gemEntity.getName(); for (int i = 0; i < parentNode.getChildCount(); i++) { GemTreeNode child = ((GemTreeNode) parentNode.getChildAt(i)); QualifiedName childName = ((GemEntity)child.getUserObject()).getName(); if (childName.equals(entityName)) { return child; } } return null; } /** * Obtain the entity for a Gem from a BrowserTree path. * @return the entity of the Gem or NULL if not a gem (eg. a drawer or workspace) * @param path the path to the Gem */ private static GemEntity getEntityFromPath(TreePath path) { // First we can check if this is a Gem. The endpoint must be a terminator node TreeNode endpoint = (TreeNode)path.getLastPathComponent(); if (endpoint.getAllowsChildren()) { // No good, this is not a terminator return null; } return (GemEntity)((BrowserTreeNode)endpoint).getUserObject(); } /** * Sets whether or not the gems in the BrowserTree can be involved with Drag and Drop. * Creation date: (06/04/01 12:01:00 PM) * @param allowed boolean */ public void setEnabledDragAndDrop(boolean allowed) { isDraggable = allowed; } /** * Sets whether or not type expressions are displayed next to gem names in the tree. * @param display */ public void setDisplayTypeExpr(boolean display) { displayTypeExpr = display; updateUI(); } /** * Gets whether or not type expressions are displayed next to gem names in the tree. * @return true if type expressions are displayed */ public boolean getDisplayTypeExpr() { return displayTypeExpr; } /** * Sets whether the current module should be highlighted in the tree. * @param highlightCurrentModule */ public void setHighlightCurrentModule(boolean highlightCurrentModule) { this.highlightCurrentModule = highlightCurrentModule; } /** * Return whether the current module should be highlighted in the tree. * @return whether the current module should be highlighted in the tree. */ boolean getHighlightCurrentModule() { return highlightCurrentModule; } /** * Set the provider of popups to this tree. * @param newMenuProvider the new popup menu provider. */ public void setPopupMenuProvider(PopupMenuProvider newMenuProvider) { this.popupMenuProvider = newMenuProvider; } /** * Return the BrowserTreeActions object associated with this tree * @return BrowserTreeActions */ public BrowserTreeActions getBrowserTreeActions() { return browserTreeActions; } /** * Return the current drag image if the gem browser is the source of the drag. * Creation date: (03/15/01 3:48:00 PM) * @return BufferedImage the image to drag, or null if the browser is not the source of a drag. */ public BufferedImage getDragImage() { return dragAndDropHandler.dragImage; } /** * Return the current drag offset if the gem browser is the source of the drag. * Creation date: (03/15/01 3:51:00 PM) * @return Point the current drag offset. */ public Point getDragOffset() { return new Point(dragAndDropHandler.mousePointOffset); } /** * Add a LeafNodeTriggeredListener to the listener list. * Creation date: (04/17/2002 10:06:00 AM). * @param listener LeafNodeTriggeredListener - the listener to add */ public void addLeafNodeTriggeredListener(LeafNodeTriggeredListener listener) { if (listener != null) { leafNodeTriggeredListeners.add(listener); } } /** * Notify the appropriate registered listeners that a node has been triggered. * Creation date: (04/17/2002 10:08:00 AM). * @param userObjects the objects of the nodes that were triggered */ protected void fireLeafNodeTriggeredEvent(List<Object> userObjects) { // Create the LeafNodeTriggeredEvent LeafNodeTriggeredEvent event = new LeafNodeTriggeredEvent(this, userObjects); // Notify each listener for (int i = 0, listenerCount = leafNodeTriggeredListeners.size(); i < listenerCount; i++) { leafNodeTriggeredListeners.get(i).leafNodeTriggered(event); } } /** * @see javax.swing.JComponent#getToolTipText(java.awt.event.MouseEvent) */ @Override public String getToolTipText(MouseEvent mouseEvent) { Point mouseLocation = mouseEvent.getPoint(); TreePath path = getPathForLocation(mouseLocation.x, mouseLocation.y); if (path != null) { BrowserTreeNode node = (BrowserTreeNode) path.getLastPathComponent(); if (node instanceof GemTreeNode && node.isLeaf ()) { BrowserTreeModel browserTreeModel = (BrowserTreeModel) getModel(); ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(browserTreeModel.getPerspective().getWorkingModuleTypeInfo()); GemEntity gemEntity = (GemEntity)node.getUserObject(); return ToolTipHelpers.getEntityToolTip(gemEntity, namingPolicy, this); } else if (node instanceof GemDrawer) { BrowserTreeModel browserTreeModel = (BrowserTreeModel)getModel(); GemDrawer gemDrawer = (GemDrawer)node; ModuleName moduleName = gemDrawer.getModuleName(); // Display visibility description in tooltip String drawerVisibility; if (!browserTreeModel.isVisibleModule(moduleName)) { drawerVisibility = BrowserMessages.getString("GB_NotImported"); } else if (browserTreeModel.isWorkingModule(moduleName)) { drawerVisibility = BrowserMessages.getString("GB_CurrentlyEdited"); } else { drawerVisibility = BrowserMessages.getString("GB_Imported"); } StringBuilder toolTipText = new StringBuilder("<html><b>" + node.getDisplayedString() + "</b> (" + drawerVisibility + ")<br>"); toolTipText.append(node.getSecondaryToolTipText()); toolTipText.append("</html>"); return toolTipText.toString(); } else { return node.getToolTipText(); } } return null; } /** * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent) */ @Override public Point getToolTipLocation(MouseEvent e) { Point mouseLocation = e.getPoint(); TreePath path = getPathForLocation(mouseLocation.x, mouseLocation.y); if (path != null) { Rectangle bounds = getPathBounds(path); if (bounds != null) { Rectangle visibleRect = getVisibleRect(); // always display tooltip along the right side of the tree int x = visibleRect.x + visibleRect.width - 20; return new Point (x > 0 ? x : 0, bounds.y); } } return null; } }