/* * 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. */ /* * HighlightTree.java * Creation date: Jun 8, 2004. * By: Edward Lam */ package org.openquark.util.ui; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Paint; import java.awt.Point; import java.awt.Rectangle; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.dnd.Autoscroll; import java.awt.dnd.DnDConstants; 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.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; import javax.swing.JViewport; import javax.swing.Timer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; /** * A subclass of JTree that can display a background image and a separator during drag-and-drop. * @author Edward Lam */ public abstract class HighlightTree extends UtilTree implements Autoscroll { /** The shared DnD handler that rejects all drop operations */ protected static final RejectDropDnDHandler REJECT_DROP_HANDLER = new RejectDropDnDHandler(); /** The Insets used by getAutoscrollInsets(). */ private static final Insets AUTOSCROLL_INSETS = new Insets(8, 8, 8, 8); /** If non-null, the background image to display in the tree. */ private ImageIcon backgroundImage; /** This menu will be displayed when the right mouse click occurs away from any of the tree nodes. */ private JPopupMenu defaultContextMenu; /** The number of milliseconds to wait before expanding a path */ private int expansionDelay = 500; // default: 500 ms /** The drop target listener responsible for creating highlights in the tree. */ private final HighlightTreeDropTargetListener dropTargetListener; /** * A special <code>HighlightTreeDnDHandler</code> that rejects all drop operations. */ private static class RejectDropDnDHandler implements HighlightTreeDnDHandler { /** {@inheritDoc} */ public boolean canAcceptDataFlavor(DataFlavor[] flavors) { return false; } /** {@inheritDoc} */ public boolean doDrop(Transferable transferable, Point mousePoint, TreePath path, boolean forceDialog) { return false; } /** {@inheritDoc} */ public boolean doDrop(Transferable transferable, TreePath dropNodePath, boolean upperHalf, boolean onIcon) { return false; } /** {@inheritDoc} */ public int getDropActions(DataFlavor[] transferFlavors, int dropAction, TreePath path, Point location) { return 0; } /** {@inheritDoc} */ public TreePath getPathForDrop(DropTargetDragEvent dtde) { return null; } } /** * The drop target listener for the highlight tree. * @author Edward Lam */ private class HighlightTreeDropTargetListener implements DropTargetListener { /** Flag to ignore events which result from drag-over events. */ private boolean trackingDrop = false; /** The path on which the current drag operation will drop. */ private TreePath pathForDrop = null; /** The current highlighter for the highlight tree. */ private TreeHighlighter highlighter = null; /** A timer that expand the saved selection path when time is up! */ private Timer expandTimer; /** A reference to the tree path that will be expanded */ private TreePath expandPath; /** * @see java.awt.dnd.DropTargetListener#dragEnter(DropTargetDragEvent) */ public void dragEnter(DropTargetDragEvent dtde) { // Decide if this drag should be accepted based on its data flavors if (getHighlightTreeDnDHandler().canAcceptDataFlavor(dtde.getCurrentDataFlavors())) { pathForDrop = getHighlightTreeDnDHandler().getPathForDrop(dtde); highlighter = createHighlighter(); dtde.acceptDrag(dtde.getDropAction()); } else { dtde.rejectDrag(); } } /** * @see java.awt.dnd.DropTargetListener#dragExit(DropTargetEvent) */ public void dragExit(DropTargetEvent dte) { // Reset the timer since the mouse moving away from the drop zone resetTimer(); if (highlighter != null) { highlighter.restore(); highlighter = null; } } /** * @see java.awt.dnd.DropTargetListener#drop(DropTargetDropEvent) */ public void drop(DropTargetDropEvent dtde) { Point location = dtde.getLocation(); TreePath path = getClosestPathForLocation(location.x, location.y); if (path != null) { int dropActions = getHighlightTreeDnDHandler().getDropActions(dtde.getCurrentDataFlavors(), dtde.getDropAction(), path, location); if (dropActions != DnDConstants.ACTION_NONE) { dtde.acceptDrop(dropActions); highlighter.restore (); highlighter = null; Transferable t = dtde.getTransferable(); if (pathForDrop != null) { // Interpret a 'copy' drop as the user requesting a dialog to appear even if it isn't // strictly necessary boolean forceDialog = (dtde.getDropAction() & DnDConstants.ACTION_COPY) != 0; Point mousePoint = new Point (dtde.getLocation()); dtde.dropComplete(getHighlightTreeDnDHandler().doDrop(t, mousePoint, pathForDrop, forceDialog)); } else { TreeGapHighlighter.HitTestInfo hitTestInfo = TreeGapHighlighter.hitTest(HighlightTree.this, location); if (hitTestInfo != null) { TreePath dropNodePath = getPathForRow(hitTestInfo.row); dtde.dropComplete(getHighlightTreeDnDHandler().doDrop(t, dropNodePath, hitTestInfo.upperHalf, hitTestInfo.inIcon)); } } return; } } dtde.rejectDrop(); } /** * Stops the timer if it is currently running and reset all expansion timer * related variables. */ private void resetTimer() { if (expandTimer != null) { expandTimer.stop(); } expandTimer = null; expandPath = null; } /** * Starts the timer that will expand the given tree path. * @param expandThisPath */ private void startTimer(final TreePath expandThisPath) { // If the tree path points to a leaf node, then there is no need // to start a timer TreeNode node = (TreeNode) expandThisPath.getLastPathComponent(); if (node.getChildCount() == 0) { return; } // Otherwise, start a timer. Note that this timer might get reset // if the expansion path is changed. expandTimer = new Timer(getPathExpansionDelay(), new ActionListener() { public void actionPerformed(ActionEvent e) { expandPath(expandThisPath); resetTimer(); } }); expandTimer.setCoalesce(true); expandTimer.setRepeats(false); expandTimer.start(); expandPath = expandThisPath; } /** * @see java.awt.dnd.DropTargetListener#dragOver(DropTargetDragEvent) */ public void dragOver(DropTargetDragEvent dtde) { Point location = dtde.getLocation(); TreePath path = getClosestPathForLocation(location.x, location.y); if (path == null) { dtde.rejectDrag(); return; } // If the new path is different from the expansion path that is associated // with the current timer, then kills the timer if (expandPath != null && !expandPath.equals(path)) { resetTimer(); } // Start the timer to expand the path that is closest to the current // mouse location. Do not start a new timer again if the expansion // path is not changed! if (expandTimer == null) { startTimer(path); } trackingDrop = true; updateForDrag(dtde); // The highlighter can be null if we don't support any of the drag flavours present during // the drag enter event if (highlighter != null) { // TEMP: if the drop path is non-null, adjust the location so that it is in the center of the path bounds. if (pathForDrop != null) { Rectangle bounds = getPathBounds(pathForDrop); location = new Point((int)bounds.getCenterX(), (int)bounds.getCenterY()); } highlighter.showHighlight(location); } trackingDrop = false; int possibleDropActions = getHighlightTreeDnDHandler().getDropActions(dtde.getCurrentDataFlavors(), dtde.getDropAction(), path, location); if (possibleDropActions != DnDConstants.ACTION_NONE) { dtde.acceptDrag(possibleDropActions); } else { dtde.rejectDrag(); } } /** * Update the highlighter state based on the current drag. */ private void updateForDrag(DropTargetDragEvent dtde) { TreePath newPathForDrop = getHighlightTreeDnDHandler().getPathForDrop(dtde); if (pathForDrop != newPathForDrop) { if (highlighter != null) { highlighter.eraseHighlight(); } pathForDrop = newPathForDrop; highlighter = createHighlighter(); } } /** * @see java.awt.dnd.DropTargetListener#dropActionChanged(DropTargetDragEvent) */ public void dropActionChanged(DropTargetDragEvent dtde) { dtde.acceptDrag(dtde.getDropAction()); } public boolean isTrackingDrop() { return trackingDrop; } private TreeHighlighter createHighlighter () { if (pathForDrop != null) { return new TreeNodeHighlighter (HighlightTree.this); } else { return new TreeGapHighlighter (HighlightTree.this); } } void eraseHighlight () { if (highlighter != null) { highlighter.eraseHighlight(); } } } /** * Constructor for HighlightTree. */ public HighlightTree() { this(new DefaultTreeModel(null)); } /** * Constructor for HighlightTree. */ public HighlightTree(DefaultTreeModel model) { super(model); getSelectionModel().setSelectionMode (TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); this.setCellRenderer(new DefaultHighlightTreeCellRenderer()); // Prevent the component from drawing its own background // so that a background image can be drawn. this.setOpaque(false); this.dropTargetListener = new HighlightTreeDropTargetListener(); new DropTarget(this, dropTargetListener); } /** * Returns the number of milliseconds to wait before expanding a tree path. * @return int */ public int getPathExpansionDelay() { return expansionDelay; } /** * Sets the number of milliseconds to wait before expanding a tree path. * By default, this number is set at 750. Negative numbers will be treated * as zeroes. * @param expansionDelay */ public void setPathExpansionDelay(int expansionDelay) { this.expansionDelay = Math.max(0, expansionDelay); } /** * Returns the drop-and-drop handler for this <code>HighlightTree</code>. * Subclasses must override this method to provide a non-null implementation * of the <code>HighlightTreeDnDHandler</code> interface. If a subclass * simply wants to disable dropping items on this tree, then return the * shared handler instance defined in this class: REJECT_DROP_HANDLER. * However, if this is the case, then it might be better for the developer * to create its tree implementation from a JTree instead. * @return the drag-and-drop handler of the highlight tree. */ protected abstract HighlightTreeDnDHandler getHighlightTreeDnDHandler(); // public String getToolTipText (MouseEvent event) { // // Determine the node at this location in the tree. // TreePath path = getClosestPathForLocation (event.getX (), event.getY ()); // // if (path != null) { // // Add a new node to the nearest node in the tree. // DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent (); // return node.getUserObject ().toString (); // } // // return null; // } /** * @see java.awt.dnd.Autoscroll#getAutoscrollInsets() */ public Insets getAutoscrollInsets() { // return new Insets (5, 5, 5, 5); Rectangle r = getVisibleRect(); Dimension size = getSize(); Insets i = new Insets( r.y + AUTOSCROLL_INSETS.top, r.x + AUTOSCROLL_INSETS.left, size.height - r.y - r.height + AUTOSCROLL_INSETS.bottom, size.width - r.x - r.width + AUTOSCROLL_INSETS.right); return i; } /** * @see java.awt.dnd.Autoscroll#autoscroll(java.awt.Point) */ public void autoscroll(Point location) { Dimension size = getSize(); int left = Math.max(location.x - AUTOSCROLL_INSETS.left, 0), right = Math.min (location.x + AUTOSCROLL_INSETS.right, size.width), top = Math.max(location.y - AUTOSCROLL_INSETS.top, 0), bottom = Math.min (location.y + AUTOSCROLL_INSETS.bottom, size.height); if (left >= right || top >= bottom) { return; } Rectangle rect = new Rectangle (left, top, right - left, bottom - top); Rectangle visibleRect = getVisibleRect(); if (!visibleRect.contains(rect)) { dropTargetListener.eraseHighlight(); scrollRectToVisible(rect); } } /* (non-Javadoc) * @see java.awt.Component#processMouseEvent(java.awt.event.MouseEvent) */ @Override protected void processMouseEvent(MouseEvent e) { super.processMouseEvent(e); if (e.isPopupTrigger()) { // 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. TreePath treePath = getPathForLocation(e.getX(), e.getY()); if (treePath == null || !isPathSelected(treePath)) { setSelectionPath(treePath); } // Check whether there is a pop-up menu for the current selection. JPopupMenu menu = getContextMenu(treePath); if (menu != null) { menu.show(this, e.getX(), e.getY()); } else if (defaultContextMenu != null){ // If the click isn't over a node in the tree, then display // a popup menu with options for the outline view. defaultContextMenu.show(this, e.getX(), e.getY()); } } } /** * Get the popup menu corresponding to a given path. * @param invocationPath the path on which the popup menu was invoked. * @return the corresponding popup menu, or null to show the default popup menu. */ protected JPopupMenu getContextMenu(TreePath invocationPath) { return null; } /** * Sets the context menu to be displayed when the mouse is clicked * away from any of the tree nodes. */ public void setDefaultContextMenu(JPopupMenu defaultContextMenu) { this.defaultContextMenu = defaultContextMenu; } protected abstract boolean canDropOnIcon(int row); /** * Method isTrackingDrop. * @return boolean */ public boolean isTrackingDrop() { return dropTargetListener.isTrackingDrop(); } /** * Returns the image to be displayed in the background of the outline view. */ public ImageIcon getBackgroundImage() { return backgroundImage; } /** * Sets the image to be displayed in the background of the outline view. */ public void setBackgroundImage(ImageIcon image) { this.backgroundImage = image; repaint(); } /** * @see javax.swing.JComponent#paintComponent(Graphics) */ @Override protected void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; Paint oldPaint = g2.getPaint(); g2.setPaint(getBackground()); try { // Fill in the background of the component. g2.fillRect(0, 0, getWidth(), getHeight()); // Draw the background image, if any. ImageIcon backgroundImage = getBackgroundImage(); if (backgroundImage != null) { final int imageX; final int imageY; // If the tree is contained within a scroll pane, then keep the background // image in the center of the view. Component parent = getParent(); if (parent instanceof JViewport) { JViewport viewport = (JViewport) parent; Rectangle viewRect = viewport.getViewRect(); imageX = viewRect.x + (viewRect.width - backgroundImage.getIconWidth()) / 2; imageY = viewRect.y + (viewRect.height - backgroundImage.getIconHeight()) / 2; } else { imageX = (getWidth() - backgroundImage.getIconWidth()) / 2; imageY = (getHeight() - backgroundImage.getIconHeight()) / 2; } g2.drawImage (backgroundImage.getImage(), imageX, imageY, backgroundImage.getIconWidth(), backgroundImage.getIconHeight(), getBackground(), null); } } finally { g2.setPaint(oldPaint); } super.paintComponent(g); } }