/* treeControl.java A hierarchical tree panel for Java This component allows the display of a tree structured graph of nodes, each node being a small image and a line of text. Nodes with children can be opened or closed, allowing the child nodes to be made visible or hidden. Each node can be selected and can have a pop-up menu attached. Nodes can be dragged, with both 'drag-tween' and 'drag on' drag supported. Created: 3 March 1997 Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu ----------------------------------------------------------------------- Copyright (C) 1996-2010 The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.JTree; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.lang.System; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.util.Stack; import java.util.Vector; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.KeyStroke; /*------------------------------------------------------------------------------ class treeControl ------------------------------------------------------------------------------*/ /** * This component allows the display of a tree structured graph of * nodes, each node being a small image and a line of text. Nodes * with children can be opened or closed, allowing the child nodes to * be made visible or hidden. Each node can be selected and can have * a pop-up menu attached. Nodes can be dragged, with both * 'drag-tween' and 'drag on' drag supported. * * @author Jonathan Abbey * * @see arlut.csd.JTree.treeCallback * @see arlut.csd.JTree.treeNode */ public class treeControl extends JPanel implements AdjustmentListener, ActionListener, FocusListener, MouseWheelListener { static final boolean debug = false; static final int borderSpace = 0; // drag mode codes, used as bit masks public static final int DRAG_NONE = 0; public static final int DRAG_ICON = 1; public static final int DRAG_LINE = 2; // --- // general tree datastructures treeNode root; treeCallback callback; treeCanvas canvas; treeNode selectedNode = null; // drag and drop support treeDragDropCallback dCallback = null; int dragMode = DRAG_NONE; treeNode oldNode = null, dragNode = null, dragOverNode = null, // used in DRAG_ICON mode dragBelowNode = null, // used in DRAG_LINE mode dragAboveNode = null; // used in DRAG_LINE mode // popup menu support treeMenu menu; // default popup menu attached to tree as a whole // housekeeping JScrollBar hbar, vbar; Rectangle bounding_rect; boolean hbar_visible, vbar_visible; int minWidth = 50, maxWidth = 0, // how wide is our tree at its maximum width currently? row_height; // how tall is a row of our tree, all told? Vector rows; // map visible rows to nodes in the tree treeNode menuedNode = null; // node most recently selected by a popup menu /* -- */ /** * Constructor * * @param font Font for text in the Tree Canvas * @param fgColor Foreground color for text in the treeCanvas * @param bgColor Background color for text in the treeCanvas * @param callback Object to receive notification of events * @param images Array of images to be used in the canvas; nodes refer to images by index * @param menu Popup menu to attach to the treeControl */ public treeControl(Font font, Color fgColor, Color bgColor, treeCallback callback, Image[] images, treeMenu menu) { super(false); // no double buffering for us, we'll do it ourselves /* Initialize our bounding rectangle. This will get filled when the AWT calls our setBounds() method. */ this.callback = callback; this.menu = menu; bounding_rect = new Rectangle(); setLayout(new BorderLayout()); canvas = new treeCanvas(this, font, fgColor, bgColor, images); add("Center", canvas); // create our scroll bars, but don't add them to our // container until we know we need them. hbar = new JScrollBar(JScrollBar.HORIZONTAL); hbar.addAdjustmentListener(this); vbar = new JScrollBar(JScrollBar.VERTICAL); vbar.addAdjustmentListener(this); // register the popup menu on ourselves if (menu!= null) { if (menu.registerItems(this)) { canvas.add(menu); } } rows = new Vector(); addMouseWheelListener(this); addKeyListener(new myKeyListener()); initializeKeyboardActions(); setFocusable(true); addFocusListener(this); } /** * Constructor * * @param font Font for text in the Tree Canvas * @param fgColor Foreground color for text in the treeCanvas * @param bgColor Background color for text in the treeCanvas * @param callback Object to receive notification of events * @param images Array of images to be used in the canvas; nodes refer to images by index */ public treeControl(Font font, Color fgColor, Color bgColor, treeCallback callback, Image[] images) { this(font, fgColor, bgColor, callback, images, null); } /** * This method is used to set the drag behavior of the tree. * * @param dCallback DragDrop manager object * @param mode A binary or'ing of of treeControl.DRAG_NONE, treeControl.DRAG_ICON, * and treeControl.DRAG_LINE. * * @see arlut.csd.JTree.treeDragDropCallback */ public synchronized void setDrag(treeDragDropCallback dCallback, int mode) { this.dCallback = dCallback; if (dCallback == null) { dragMode = DRAG_NONE; } else { dragMode = mode; } } /** * <p>Call this on the GUI thread to break apart and destroy this * tree.</p> */ public synchronized void destroyTree() { this.callback = null; this.clearTree(); this.removeAll(); } /** * Clear out the tree. */ public synchronized void clearTree() { if (root == null) { return; } // get rid of the nodes breakdownTree(root); root = null; // clear our visibility vector unselectAllNodes(false); if (rows != null) { rows.removeAllElements(); } else { rows = new Vector(); // shouldn't ever be the case, but just // to be safe.. } refresh(); } /** * Clear the tree and establish a new root node. */ public synchronized void setRoot(treeNode root) { if (root == null) { throw new IllegalArgumentException("can't setRoot(null), use clearTree"); } unselectAllNodes(false); if (this.root != null) { breakdownTree(this.root); } if (rows != null) { rows.removeAllElements(); } else { rows = new Vector(); } // let the node know who it is attached to root.tree = this; this.root = root; root.row = 0; if (root.menu != null) { if (root.menu.registerItems(this)) { canvas.add(root.menu); } } rows.addElement(root); selectNode(root); reShape(); refreshTree(); } /** * Sets the minimum width the treeControl will demand in a sliding * pane. */ public void setMinimumWidth(int minWidth) { this.minWidth = minWidth; } /** * Inserts a new node into the tree. newNode's prevSibling is * checked first. If it is non-null, newNode is inserted after * newNode.prevSibling, regardless of what newNode.parent says. If * prevSibling is null, newNode is made the first child of its * requested parent. * * @param newNode The node to be inserted. Properties of the node determine where the node is inserted. * @param repaint If true, immediately re-render and refresh the treeCanvas. */ public synchronized void insertNode(treeNode newNode, boolean repaint) { treeNode p; /* -- */ if (debug) { System.err.println("treeControl: Inserting node " + newNode.getText() + ", repaint is " + repaint); } newNode.childStack = null; if (newNode.prevSibling == null && newNode.parent == null) { throw new IllegalArgumentException("error, no insert after or parent set. Use setRoot to establish root node"); } // let the node know who we are, so it can change // its menu later newNode.tree = this; // attach the menu to us if (newNode.menu != null) { if (newNode.menu.registerItems(this)) { canvas.add(newNode.menu); } } if (newNode.prevSibling != null) { // if we're being inserted after the last sibling // in the family and they had a childStack // constructed, we need to invalidate that if (newNode.prevSibling.nextSibling == null && newNode.prevSibling.childStack != null) { clearStacks(newNode.prevSibling); // not newNode.prevSibling.child! } // if our prevSibling was visible, we're going to be // visible too. We just need to figure out where // we are in relation to our sibling if (newNode.prevSibling.row != -1) { if (newNode.prevSibling.child == null || !newNode.prevSibling.expanded) { // we're going to be the next row newNode.row = newNode.prevSibling.row + 1; rows.insertElementAt(newNode, newNode.row); } else { // we're going to be the row after all the kiddies // create an empty range.. getVisibleDescendantRange will // expand this to represent the span taken by the visible // children of newNode.prevSibling. Range range = new Range(newNode.prevSibling.child.row, newNode.prevSibling.child.row); getVisibleDescendantRange(newNode.prevSibling.child, range); // and we're next after range.high newNode.row = range.high + 1; rows.insertElementAt(newNode, newNode.row); } // adjust everybody below for (int i = newNode.row + 1; i < rows.size(); i++) { p = (treeNode) rows.elementAt(i); p.row = i; } } else { newNode.row = -1; } newNode.nextSibling = newNode.prevSibling.nextSibling; newNode.prevSibling.nextSibling = newNode; if (newNode.nextSibling != null) { newNode.nextSibling.prevSibling = newNode; } newNode.parent = newNode.prevSibling.parent; } else if (newNode.parent != null) { // insert us right before the first child // node of our parent newNode.nextSibling = newNode.parent.child; if (newNode.nextSibling != null) { newNode.nextSibling.prevSibling = newNode; } newNode.parent.child = newNode; // Figure out our row and adjust all rows below // us if we're visible if (newNode.parent.row != -1 && newNode.parent.expanded) { newNode.row = newNode.parent.row + 1; rows.insertElementAt(newNode, newNode.row); for (int i = newNode.row + 1; i < rows.size(); i++) { p = (treeNode) rows.elementAt(i); p.row = i; } } else { // oops, not visible newNode.row = -1; } } if (repaint) { refresh(); } } /** * <p>Removes a node from the tree, along with all its children. * Any child nodes attached to the node to be deleted will be * unlinked from one another. If you want to be able to re-insert * the deleted node elsewhere in the tree, you probably should use * moveNode() instead.</p> * * @param node The node to be removed. * @param repaint If true, immediately re-render and refresh the treeCanvas. */ public synchronized void deleteNode(treeNode node, boolean repaint) { treeNode p; /* -- */ // if we're visible and have children visible, contract to // hide those if (node.selected) { unselectNode(node, false); } if (node.row != -1) { if (node.expanded) { contractNode(node, false); } } // if our removal would change the picture for our prevSibling's // children in the render algorithm, null out the childStacks // to force a recalc if (node.nextSibling == null && node.prevSibling != null && node.prevSibling.child != null) { clearStacks(node.prevSibling); } // unlink us from the tree if (node.parent != null) { if (node.prevSibling == null) { node.parent.child = node.nextSibling; } } else { root = node.nextSibling; } if (node.prevSibling != null) { node.prevSibling.nextSibling = node.nextSibling; } if (node.nextSibling != null) { node.nextSibling.prevSibling = node.prevSibling; } // if node.row == -1, the node is not currently visible for // display. if (node.row != -1) { rows.removeElementAt(node.row); // move everybody else up for (int i = node.row; i < rows.size(); i++) { p = (treeNode) rows.elementAt(i); p.row = i; } } // clean things out for GC. breakdownTree(node.child); node.tree = null; node.row = -1; node.parent = null; node.childStack = null; node.text = null; node.menu = null; if (repaint) { refresh(); } } /** * Moves a node (possibly the root of an extensive subtree) from one * location in the tree to another.<br><br> * * Note that this method is currently implemented in a fairly simplistic * manner, using the deleteNode and insertNode primitives, cloning nodes * as they are copied into the new location in the tree. This works reliably, * but this might not be the best implementation for moving large sub-trees. * * @param node The node to be moved. * @param parent Parent node to insert this node under, null if this is a top-level node * @param insertAfter sibling to insert this node after * @param repaint If true, immediately re-render and refresh the treeCanvas after moving the node. * * @return a reference to a copy of the node in its new location */ public synchronized treeNode moveNode(treeNode node, treeNode parent, treeNode insertAfter, boolean repaint) { if (insertAfter == null && parent == null) { throw new IllegalArgumentException("error, no insert after or parent set. Use setRoot to establish root node"); } if (node == null) { throw new IllegalArgumentException("can't move null"); } if (node == parent) { throw new IllegalArgumentException("error, attempt made to move node under itself."); } /* - */ treeNode newNode, child, oldchild, nextchild; /* -- */ // make a copy of the node to put into the new position. we clone // it so that we can get any additional data-carrying fields held // by subclasses of treeNode, even though the resetNode() call // immediately after clears the node of all of its previous // linkages newNode = (treeNode) node.clone(); newNode.resetNode(); newNode.parent = parent; newNode.prevSibling = insertAfter; insertNode(newNode, false); if (node.isOpen()) { expandNode(newNode, false, false); } // now, if the node we moved has children, move them over too. oldchild = null; child = node.child; while (child != null) { nextchild = child.nextSibling; oldchild = moveNode(child, newNode, oldchild, false); child = nextchild; } // now take the node we moved out of the tree deleteNode(node, false); if (repaint) { refresh(); } return newNode; } /** * Removes all children of the specified node from the tree. * * @param node The node whose children should be removed * @param repaint If true, immediately re-render and refresh the treeCanvas. */ public synchronized void removeChildren(treeNode node, boolean repaint) { // if we're visible and have children visible, contract to // hide those if (node.row != -1) { if (node.expanded) { contractNode(node, false); } } breakdownTree(node.child); node.child = null; if (repaint) { refresh(); } } /** * Helper function to break down links in a tree to speed GC. * breakdownTree will disassociate all nodes in an unlinked * tree, so new nodes will need to be assembled in order * to be resubmitted to the tree. */ void breakdownTree(treeNode node) { if (node == null) { return; } node.tree = null; node.row = -1; node.text = null; node.childStack = null; node.menu = null; node.parent = null; breakdownTree(node.child); node.child = null; breakdownTree(node.nextSibling); node.nextSibling = null; node.prevSibling = null; node.cleanup(); } /** * Helper method to force recalculation of childStacks. This method * should be called on the child of a node, not on the node itself, * lest clearStacks recurse along the node's siblings.. not that * that will cause any great hardship, but it would slow things down * a very little bit. */ private void clearStacks(treeNode node) { if (node == null) { return; } if (node.child != null) { node.childStack = null; clearStacks(node.child); } clearStacks(node.nextSibling); } /** * Get access to the root of the treeCanvas's tree of nodes. */ public treeNode getRoot() { return root; } /** * Recalculate and redraw the tree. */ public void refresh() { reShape(); refreshTree(); } /** * Internal diagnostic method, prints the structure of this tree to * System.err. */ public void dumpRows() { treeNode p; for (int i = 0; i < rows.size(); i++) { p = (treeNode) rows.elementAt(i); System.err.print("row " + i + ": " + p.text + ", "); if (p.parent != null) { System.err.println(p.parent.childStack.size() + "levels in"); } else { System.err.println("top level"); } if (p.prevSibling != null) { System.err.print("previous sibling = " + p.prevSibling.getText() + " "); } if (p.nextSibling != null) { System.err.println("next sibling = " + p.nextSibling.getText()); } } } // internal methods, called by treeCanvas /** * open the given node */ public void expandNode(treeNode node, boolean repaint) { expandNode(node, repaint, true); } /** * open the given node */ public synchronized void expandNode(treeNode node, boolean repaint, boolean doCallback) { int row; treeNode p; /* -- */ if (node.expanded) { return; } node.expanded = true; row = makeDescendantsVisible(node.child, node.row + 1) + 1; // adjust everything below the expanded rows for (int i = row; i < rows.size(); i++) { p = (treeNode) rows.elementAt(i); p.row = i; } if (repaint) { refresh(); } if (doCallback && callback != null) { callback.treeNodeExpanded(node); } } /** * recursive routine to make descendant nodes visible. will not * expand any contracted nodes, but will add any nodes reachable * without passing below a contracted node to the visibility * * @param row the row to match the node to. * * @return the row number of the last descendant made visible */ private int makeDescendantsVisible(treeNode node, int row) { while (node != null) { node.row = row; rows.insertElementAt(node, row); if (node.child != null && node.expanded) { row = makeDescendantsVisible(node.child, row + 1); } node = node.nextSibling; row++; } return row-1; } /** * close the given node */ public void contractNode(treeNode node, boolean repaint) { contractNode(node, repaint, true); } /** * close the given node */ public synchronized void contractNode(treeNode node, boolean repaint, boolean doCallback) { Range range; treeNode n; /* -- */ if (!node.expanded) { return; } node.expanded = false; if (debug) { System.err.println("contractNode: row " + node.row + ", text=" + node.text); } if (node.child != null) { range = new Range(node.child.row, node.child.row); getVisibleDescendantRange(node.child, range); // remove our descendants from the visibility vector if (debug) { System.err.println("Before contraction, size = " + rows.size()); System.err.println("Contracting rows " + range.low + " through " + range.high); } for (int i = range.low; i <= range.high; i++) { n = (treeNode) rows.elementAt(range.low); if (debug) { System.err.println("Contracting row " + n.row + ", text=" + n.text); } if (n.selected) { moveSelection(node); } rows.removeElementAt(range.low); n.row = -1; } // move everybody else up for (int i = range.low; i < rows.size(); i++) { n = (treeNode) rows.elementAt(i); if (debug) { System.err.println("moving up row " + n.row + " to " + i); } n.row = i; } if (debug) { System.err.println("After contraction, size = " + rows.size()); } } if (repaint) { refresh(); } if (doCallback && callback != null) { callback.treeNodeContracted(node); } } /** * Calculates the rows that are visible at and below node, * so that contractNode() can remove all nodes in that * range from visibility * * @param node The node of the subtree we need to examine * @param range We encode the range below node in the Range object */ private void getVisibleDescendantRange(treeNode node, Range range) { treeNode myNode = node; while (myNode != null) { // a row of -1 means we've hit an invisible row, which we // never should do. if (myNode.row == -1) { throw new RuntimeException("invisible row hit"); } if (myNode.row > range.high) { range.high = myNode.row; } if (myNode.row < range.low) { range.low = myNode.row; } // if we have a sibling below us, we don't need to // check the kids if (myNode.nextSibling != null) { myNode = myNode.nextSibling; } else if (myNode.child != null && myNode.expanded) { myNode = myNode.child; } else // we're done going down { break; } } } /** * Select a node without issuing a callback to the client.<br><br> * * Used to implement highlighting during drag-and-drop. */ void transientSelectNode(treeNode node) { if (node.selected) { return; } node.selected = true; selectedNode = node; } /** * Used to trigger the default action in the callback. */ void doubleClickNode(treeNode node) { if (callback != null) { callback.treeNodeDoubleClicked(node); } } /** * Mark a node as selected, issuing a callback to the client * reporting the selection.<br><br> * * This method does not deselect other nodes. * * @param node The node to select */ public void selectNode(treeNode node) { if (node.selected) { return; } node.selected = true; selectedNode = node; if (callback != null) { callback.treeNodeSelected(node); } } /** * This method deselects the currently selected node (if any) and * selects the paramater node. * * @param node The node to select */ void moveSelection(treeNode node) { if (node.selected) { return; } if (selectedNode != null) { unselectNode(selectedNode, true); } selectNode(node); } /** * Deselect a node without issuing a callback to the client.<br><br> * * Used to implement highlighting during drag-and-drop. * * @param node The node to deselect */ void transientUnselectNode(treeNode node) { if (!node.selected) { return; } node.selected = false; selectedNode = null; } /** * Deselect a node, issuing a callback to the client * reporting the deselection.<br><br> * * @param anySelected If true, the client will be told * that some node will remain selected after this operation * is completed * @param node The node to deselect */ void unselectNode(treeNode node, boolean anySelected) { if (!node.selected) { return; } node.selected = false; selectedNode = null; if (callback != null) { callback.treeNodeUnSelected(node, anySelected); } } /** * Deselect all nodes, issuing a callback to the client * reporting the deselection.<br><br> * * @param anySelected If true, the client will be told * that some node will remain selected after this operation * is completed. */ public void unselectAllNodes(boolean anySelected) { treeNode node; /* -- */ for (int i = 0; i < rows.size(); i++) { node = (treeNode) rows.elementAt(i); if (node.selected) { unselectNode(node, anySelected); } } } /** * Handle notification from popupmenus and from key board navigation * actions * * @param e The event we're receiving as an ActionListener */ public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof JMenuItem) { if (callback == null) { return; } if (menuedNode == null) { return; } callback.treeNodeMenuPerformed(menuedNode, e); menuedNode = null; return; } String actionCommand = e.getActionCommand(); if (actionCommand == null) { System.err.println("Null action command from " + e); return; } if (actionCommand.equals("unitdown")) { if (selectedNode != null) { int rowIndex = selectedNode.row; if (rowIndex + 1 < rows.size()) { treeNode newNode = (treeNode) rows.elementAt(rowIndex + 1); moveSelection(newNode); scrollToSelectedRow(); canvas.render(); canvas.repaint(); } } } else if (actionCommand.equals("unitup")) { if (selectedNode != null) { int rowIndex = selectedNode.row; if (rowIndex > 0) { treeNode newNode = (treeNode) rows.elementAt(rowIndex - 1); moveSelection(newNode); scrollToSelectedRow(); canvas.render(); canvas.repaint(); } } } else if (actionCommand.equals("scrolldown")) { if (selectedNode != null) { // move our selection down, scrolling if necessary int rowIndex = selectedNode.row; int jumpIncrement = (canvas.getBottomRow() - canvas.getTopRow()) - 1; int newSelectionIndex = rowIndex + jumpIncrement; if (newSelectionIndex >= rows.size()) { newSelectionIndex = rows.size() - 1; } moveSelection((treeNode) rows.elementAt(newSelectionIndex)); scrollToSelectedRow(); canvas.render(); canvas.repaint(); } else if (vbar_visible) { // no selected unit, just scroll int adj = vbar.getBlockIncrement(); int presentValue = vbar.getValue(); int maxValue = rows.size() * row_height - canvas.getBounds().height; if (presentValue + adj > maxValue) { presentValue = maxValue; } else { presentValue = presentValue + adj; } vbar.setValue(presentValue); } } else if (actionCommand.equals("scrollup")) { if (selectedNode != null) { // move our selection down, scrolling if necessary int rowIndex = selectedNode.row; int jumpIncrement = (canvas.getBottomRow() - canvas.getTopRow()) - 1; int newSelectionIndex = rowIndex - jumpIncrement; if (newSelectionIndex <= 0) { newSelectionIndex = 0; } moveSelection((treeNode) rows.elementAt(newSelectionIndex)); scrollToSelectedRow(); canvas.render(); canvas.repaint(); } else if (vbar_visible) { // no selected unit, just scroll int adj = vbar.getBlockIncrement(); int presentValue = vbar.getValue(); if (presentValue - adj < 0) { presentValue = 0; } else { presentValue = presentValue - adj; } vbar.setValue(presentValue); } } else if (actionCommand.equals("scrolltop")) { moveSelection((treeNode) rows.elementAt(0)); scrollToSelectedRow(); canvas.render(); canvas.repaint(); } else if (actionCommand.equals("scrollbottom")) { moveSelection((treeNode) rows.elementAt(rows.size() - 1)); scrollToSelectedRow(); canvas.render(); canvas.repaint(); } else if (actionCommand.equals("right")) { if (selectedNode != null && selectedNode.expandable && !selectedNode.isOpen()) { expandNode(selectedNode, true); } } else if (actionCommand.equals("left")) { if (selectedNode != null) { if (selectedNode.expandable && selectedNode.isOpen()) { contractNode(selectedNode, true); } else if (selectedNode.getParent() != null) { moveSelection(selectedNode.getParent()); scrollToSelectedRow(); canvas.render(); canvas.repaint(); } } } else if (actionCommand.equals("enter")) { if (selectedNode != null) { if (selectedNode.expandable) { expandNode(selectedNode, true); } else { doubleClickNode(selectedNode); } } } else if (actionCommand.equals("context")) { if (selectedNode != null) { int x = 50; int y = selectedNode.row * row_height - canvas.v_offset + (row_height / 2); treeNode tempNode = selectedNode; while (tempNode != null) { x = x + canvas.tabStep; tempNode = tempNode.getParent(); } MouseEvent me = new MouseEvent(this, 0, System.currentTimeMillis(), 0, x, y, 1, true); canvas.popupHandler(me, selectedNode); } } } /** * This method scrolls the treeCanvas so that the selected row is * visible. * * This method returns true if a scroll (and redraw) was performed, * false otherwise. */ private void scrollToSelectedRow() { if (!vbar_visible) { return; } if (selectedNode.row <= canvas.getTopRow()) { vbar.setValue(row_height * selectedNode.row); } else if (selectedNode.row + 1 >= canvas.getBottomRow()) { vbar.setValue(row_height * (selectedNode.row + 1) - canvas.getBounds().height); } } /** * Handles scrollbar events. */ public synchronized void adjustmentValueChanged (AdjustmentEvent e) { refreshTree(); } /** * This method is called when our size is changed. We need to know * this so we can update the scrollbars and what-not. */ public void resize(int x, int y, int width, int height) { setBounds(x,y,width,height); } /** * Called when we are being resized by our container. */ public synchronized void setBounds(int x, int y, int width, int height) { if (debug) { System.err.println("setBounds(): x:" + x + ", y:" + y + ", width:" + width + ", height:" + height); } super.setBounds(x,y,width,height); validate(); Rectangle rect = new Rectangle(bounding_rect); if ((width != bounding_rect.width) || (height != bounding_rect.height)) { bounding_rect.x = x; bounding_rect.y = y; bounding_rect.width = width; bounding_rect.height = height; reShape(); } if (width > rect.width || height > rect.height) { refreshTree(); } if (debug) { System.err.println("exiting setBounds()"); } } // Internal method /** * This method recalculates the general parameters of our tree's * display. That is, it calculates whether or not we need scroll * bars, adds or deletes the scroll bars, and scales the column * positions to match the general rendering parameters. */ private void reShape() { if (debug) { System.err.println("reShape()"); } // calculate whether we need scrollbars, add/remove them adjustScrollbars(); if (debug) { System.err.println("exiting reShape()"); } } // Internal method /** * Check to see whether we need scrollbars in our current component size, * set the min/max/visible parameters<br><br> * * This method is intended to be called from reShape(). */ private void adjustScrollbars() { int vSize; /* -- */ if (debug) { System.err.println("adjustScrollbars()"); System.err.println("canvas.getBounds().width = " + canvas.getBounds().width); System.err.println("canvas.getBounds().height = " + canvas.getBounds().height); } // calculate how tall or table is, not counting any scroll bars. // That is, how short can we be before we need to have a vertical // scrollbar? vSize = row_height * rows.size(); // calculate whether we need scrollbars // check to see if we need a horizontal scrollbar canvas.calcWidth(); if (canvas.getBounds().width < maxWidth) { if (!hbar_visible) { this.add("South", hbar); hbar.setValue(0); this.doLayout(); // getParent().doLayout(); } hbar_visible = true; if (debug) { System.err.println("hbar being made visible"); } } else { if (hbar_visible) { this.remove(hbar); this.doLayout(); // getParent().doLayout(); } hbar_visible = false; if (debug) { System.err.println("hbar being made INvisible"); } canvas.h_offset = 0; } // check to see if we need a vertical scrollbar if (canvas.getBounds().height < vSize) { if (!vbar_visible) { this.add("East", vbar); vbar.setValue(0); this.doLayout(); } vbar_visible = true; if (debug) { System.err.println("vbar being made visible"); } } else { if (vbar_visible) { this.remove(vbar); this.doLayout(); } vbar_visible = false; if (debug) { System.err.println("vbar being made INvisible"); } canvas.v_offset = 0; } // check again to see if we need a horizontal scrollbar.. // we need to recheck this in case adding our vertical // scrollbar squeezed us horizontally enough that we // need to put in a horizontal scrollbar if (canvas.getBounds().width < maxWidth) { if (!hbar_visible) { this.add("South", hbar); hbar.setValue(0); this.doLayout(); } hbar_visible = true; if (debug) { System.err.println("hbar being made visible"); } } else { if (hbar_visible) { this.remove(hbar); this.doLayout(); } hbar_visible = false; if (debug) { System.err.println("hbar being made INvisible"); } canvas.h_offset = 0; } // now we've got our scrollbars the way we want them as // far as visible vs. non visible.. now we need to // see about getting them properly configured // Adjust the Vertical Scrollbar's Parameters if (vbar_visible && (canvas.getBounds().height != 0)) { int my_total_height = rows.size() * row_height; int max_acceptable_value = my_total_height - canvas.getBounds().height; if (vbar.getValue() > max_acceptable_value) { vbar.setValues(max_acceptable_value, canvas.getBounds().height, 0, my_total_height); } else { vbar.setValues(vbar.getValue(), canvas.getBounds().height, 0, my_total_height); } vbar.setUnitIncrement(row_height); // we want the up/down buttons to go a line at a time vbar.setBlockIncrement(canvas.getBounds().height/2); } // Adjust the Horizontal Scrollbar's Parameters if (hbar_visible && (canvas.getBounds().width != 0)) { int max_acceptable_value = maxWidth - canvas.getBounds().width; if (hbar.getValue() > max_acceptable_value) { hbar.setValues(max_acceptable_value, canvas.getBounds().width, 0, maxWidth); } else { hbar.setValues(hbar.getValue(), canvas.getBounds().width, 0, maxWidth); } hbar.setBlockIncrement(canvas.getBounds().width / 2); } if (debug) { System.err.println("exiting adjustScrollbars()"); } } /** * Redraw the canvas. */ void refreshTree() { canvas.render(); canvas.repaint(); } /** * Method to be called after this treeControl is removed from use * and visibility. */ public synchronized void dispose() { if (canvas != null) { canvas.dispose(); canvas = null; } } // focusListener methods public synchronized void focusGained(FocusEvent e) { refreshTree(); } public synchronized void focusLost(FocusEvent e) { refreshTree(); } // MouseWheelListener public synchronized void mouseWheelMoved(MouseWheelEvent e) { if (vbar_visible) { // Scroll three lines per mousewheel turn. int adj = vbar.getUnitIncrement() * 3; int totalScrollAmount = e.getWheelRotation() * adj; vbar.setValue(vbar.getValue() + totalScrollAmount); } } private void initializeKeyboardActions() { InputMap inputMap = getInputMap(JComponent.WHEN_FOCUSED); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_DOWN, 0), "unitdown"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "unitdown"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_UP, 0), "unitup"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "unitup"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "scrolldown"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "scrollup"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), "scrolltop"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), "scrollbottom"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "right"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0), "right"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "left"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0), "left"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enter"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTEXT_MENU, 0), "context"); ActionMap actionMap = getActionMap(); actionMap.put("unitdown", new treeControlAction("unitdown")); actionMap.put("unitup", new treeControlAction("unitup")); actionMap.put("scrolldown", new treeControlAction("scrolldown")); actionMap.put("scrollup", new treeControlAction("scrollup")); actionMap.put("scrolltop", new treeControlAction("scrolltop")); actionMap.put("scrollbottom", new treeControlAction("scrollbottom")); actionMap.put("right", new treeControlAction("right")); actionMap.put("left", new treeControlAction("left")); actionMap.put("enter", new treeControlAction("enter")); actionMap.put("context", new treeControlAction("context")); } /** * This method is used by the myKeyListener on this tree to advance * the selection mark in response to data entry keyboard input from * the user. */ private synchronized void tryMatchString(String matchOn) { if (selectedNode == null) { selectNode(root); } treeNode firstNode = selectedNode; if (firstNode.expanded && firstNode.child != null) { firstNode = firstNode.child; } treeNode matchNode = firstNode; Pattern pattern = Pattern.compile("^" + matchOn, Pattern.CASE_INSENSITIVE); boolean found = pattern.matcher(matchNode.text).find() && (matchOn.length() > 1 || firstNode != selectedNode) ; while (!found) { if (matchNode.nextSibling != null) { matchNode = matchNode.nextSibling; } else if (matchNode.nextSibling == null && matchNode.parent == null) { matchNode = root; } else if (matchNode.nextSibling == null && matchNode.parent != null) { matchNode = matchNode.parent.child; } if (matchNode == firstNode) { break; } found = pattern.matcher(matchNode.text).find(); } if (found) { moveSelection(matchNode); scrollToSelectedRow(); canvas.render(); canvas.repaint(); } } /*---------------------------------------------------------------------------- inner class treeControlAction ----------------------------------------------------------------------------*/ private class treeControlAction extends javax.swing.AbstractAction { public treeControlAction(String name) { super(name); } public void actionPerformed(ActionEvent e) { ActionEvent ae = new ActionEvent(treeControl.this, 0, (String) getValue(NAME)); treeControl.this.actionPerformed(ae); } } /*---------------------------------------------------------------------------- inner class myKeyListener ----------------------------------------------------------------------------*/ private class myKeyListener implements KeyListener { /** * The time code (in milliseconds since epoch UTC) before which we * need to see the next keystroke, or else we'll clear the key buffer. */ private long matchTime = 0; /** * Milliseconds interval for continued key buffer accretion. */ private long matchTimeout = 1000; /** * The key buffer we've built up through sub-second keystroke * intervals. */ private StringBuilder matchString = new StringBuilder(); /* -- */ private myKeyListener() { } public void keyTyped(KeyEvent e) { long timeNow = java.lang.System.currentTimeMillis(); if (timeNow > matchTime) { matchString.setLength(0); } if (e.getKeyChar() == '\n' || e.getKeyChar() == '\b') { return; } matchTime = timeNow + matchTimeout; matchString.append(e.getKeyChar()); tryMatchString(matchString.toString()); } public void keyReleased(KeyEvent e) { } public void keyPressed(KeyEvent e) { if (e.isActionKey() || e.getKeyCode() == KeyEvent.VK_BACK_SPACE || e.getKeyCode() == KeyEvent.VK_DELETE || e.getKeyCode() == KeyEvent.VK_ENTER) { matchString.setLength(0); } } } } /*------------------------------------------------------------------------------ class treeCanvas ------------------------------------------------------------------------------*/ /** * <p>This class is the actual rendering surface used by the JTree {@link * arlut.csd.JTree.treeControl treeControl} class.</p> */ class treeCanvas extends JComponent implements MouseListener, MouseMotionListener { static final boolean debug = false; static final boolean debug2 = false; static final Object SPACE = new Object(); static final Object LINE = new Object(); /* - */ private treeControl ctrl; private Font font; private Color fgColor; private Color bgColor; private Color lineColor = Color.black; private Color dragLineColor = Color.black; private FontMetrics fontMetric; private int rowHeight; private int rowAscent; private int rowDescent; private int rowLeading; private Image[] images; private int maxImageHeight; private int leftSpacing = 5; protected int tabStep = 20; private int iconTextSpacing = 3; private int boxSize = 10; private Image plusBox = null; private Image minusBox = null; private Image backing; private Rectangle backing_rect; private Graphics bg; private Rectangle boundingBox = null; private Point spriteLoc = null; private boolean spriteVisible = false; private boolean dontdrag = false; private boolean drawLine = false; private boolean dragSelected = false; private Image sprite = null; int loBound = 0, hiBound = 0; int h_offset; int v_offset; /* -- */ /** * Constructor * * @param font Font for text in the Tree Canvas * @param fgColor Foreground color for text in the treeCanvas * @param bgColor Background color for text in the treeCanvas * @param images Array of images to be used in the canvas; nodes refer to images by index */ public treeCanvas(treeControl ctrl, Font font, Color fgColor, Color bgColor, Image[] images) { // super(false); // no double buffering for us, we'll do it ourselves this.ctrl = ctrl; this.font = font; this.fgColor = fgColor; this.bgColor = bgColor; this.images = images; fontMetric = getFontMetrics(font); rowAscent = fontMetric.getMaxAscent(); rowDescent = fontMetric.getMaxDescent(); rowHeight = rowAscent + rowDescent; rowLeading = fontMetric.getLeading(); maxImageHeight = 0; for (int i=0; i<images.length; i++) { int temp; temp = images[i].getHeight(this); if (temp > rowHeight) { rowHeight = temp; } if (temp > maxImageHeight) { maxImageHeight = temp; } } ctrl.row_height = rowHeight + rowLeading; // let our ctrl know the inter-line separation addMouseListener(this); addMouseMotionListener(this); ctrl.maxWidth = ctrl.minWidth; } public Dimension getMinimumSize() { return new Dimension(ctrl.minWidth, ctrl.row_height * 5); } public Dimension getPreferredSize() { Dimension result; if (ctrl.maxWidth < ctrl.minWidth) { ctrl.maxWidth = ctrl.minWidth; } if (ctrl.rows.size() < 5) { result = new Dimension(ctrl.maxWidth + ctrl.borderSpace, ctrl.row_height * 5); if (debug) { System.err.println("getPreferredSize:" + result); } return result; } else { result = new Dimension(ctrl.maxWidth + ctrl.borderSpace, ctrl.row_height * ctrl.rows.size()); if (debug) { System.err.println("getPreferredSize:" + result); } return result; } } public boolean isRequestFocusEnabled() { return true; } /** * Copy our backing store to our canvas */ public void paint(Graphics g) { if ((backing == null) || (backing.getWidth(this) < getBounds().width) || (backing.getHeight(this) < getBounds().height)) { render(); } g.drawImage(backing, 0, 0, this); if (drawLine) { int y; if (ctrl.dragBelowNode != null) { y = ctrl.dragBelowNode.row * ctrl.row_height - (rowAscent / 2) + (ctrl.row_height / 2); } else { y = (ctrl.dragAboveNode.row + 1) * ctrl.row_height - (rowAscent / 2) + (ctrl.row_height / 2); } g.setColor(dragLineColor); g.drawLine(0, y - v_offset, backing.getWidth(this), y - v_offset); } if (spriteVisible && (sprite != null) && (spriteLoc != null)) { g.drawImage(sprite, spriteLoc.x, spriteLoc.y, this); } } /** * Call our paint method without * clearing to prevent flashing */ public void update(Graphics g) { paint(g); } /** * This method returns the index of the top-most visible row in the * table. */ int getTopRow() { int v_offset; if (ctrl.vbar_visible) { v_offset = ctrl.vbar.getValue(); } else { v_offset = 0; } return v_offset / ctrl.row_height; } /** * This method returns the index of the bottom-most visible row in the * table. */ int getBottomRow() { int result = getTopRow() + (getBounds().height / ctrl.row_height) + 1; if (result > ctrl.rows.size()) { result = ctrl.rows.size(); } return result; } /** * This is our main rendering routine, which does all the work to * generate our tree image, minus the drag/drop sprite effects. */ synchronized void render() { int top_row, bottom_row; treeNode node; Vector drawVector; // we may have our bounds set with a 0 width or height by some // clueless layout managers.. homey don't play that game. if (debug) { System.err.println("Called render"); } if (!isVisible() || getBounds().width <= 0 || getBounds().height <= 0) { return; } // We don't want to construct our box images until we're sure that // the AWT has set up our peers and is really ready for us to go. if (plusBox == null || minusBox == null) { // buildBoxes will return false if our peer isn't // ready for us to create images if (!buildBoxes()) { return; } } if (backing == null) { if (debug) { System.err.println("creating backing image"); System.err.println("width = " + getBounds().width); System.err.println("height = " + getBounds().height); } backing_rect = new Rectangle(0, 0, getBounds().width, getBounds().height); backing = createImage(getBounds().width, getBounds().height); bg = backing.getGraphics(); } else if ((backing_rect.width != getBounds().width) || (backing_rect.height != getBounds().height)) { // need to get a new backing image if (debug) { System.err.println("creating new backing image"); } backing_rect = new Rectangle(0, 0, getBounds().width, getBounds().height); backing.flush(); // free old image resources backing = createImage(getBounds().width, getBounds().height); bg = backing.getGraphics(); } // set our font. bg.setFont(font); // and draw our tree if (ctrl.vbar_visible) { v_offset = ctrl.vbar.getValue(); } else { v_offset = 0; } if (ctrl.hbar_visible) { h_offset = ctrl.hbar.getValue(); } else { h_offset = 0; } bg.setColor(bgColor); bg.fillRect(0,0, getBounds().width + 1, getBounds().height + 1); top_row = v_offset / ctrl.row_height; bottom_row = top_row + (getBounds().height / ctrl.row_height) + 1; if (bottom_row >= ctrl.rows.size()) { bottom_row = ctrl.rows.size() - 1; } for (int i = top_row; i <= bottom_row; i ++) { node = (treeNode) ctrl.rows.elementAt(i); if (node.parent == null) { drawVector = null; } else if (node.parent.childStack == null) { node.parent.childStack = new Stack(); recursiveGenStack(node.parent, node.parent.childStack); drawVector = node.parent.childStack; } else { drawVector = node.parent.childStack; } drawRow(node, i, drawVector); } } /** * Internal rendering method * * Should only be called from render(), which provides the * thread-synchronized entry point. * * Generate and record information about the columns to the left of the * current node, used by drawRow to properly position each drawn node in * the hierarchy */ private void recursiveGenStack(treeNode node, Stack stack) { if (node == null) { return; } recursiveGenStack(node.parent, stack); if (node.nextSibling != null) { stack.push(LINE); } else { stack.push(SPACE); } return; } /** * drawRow() is the internal rendering method for each row of the * treeControl. * * Should only be called from render(), which provides the * thread-synchronized entry point * * @param node The treeNode corresponding to the row to be drawn * @param row The row's number in the overall tree's list of rows * @param s A Stack containing LINE and SPACE objects that need to * precede the icon and text for this node. We use this draw Stack * to properly render the containment graphics for this row. */ private void drawRow(treeNode node, int row, Vector s) { int horizLine, // y pos of this row's horizontal connecting line x1, // working variable x2, // working variable imageIndex; Image temp; /* -- */ if (debug) { if (s != null) { System.err.println("Drawing node " + node.text + ", row = " + row + "(" + node.row + "), " + s.size() + " steps in"); } else { System.err.println("Drawing node " + node.text + ", row = " + row + "(" + node.row + "), top level"); } } // this equation, especially the .75 figure, is a fudge factor to try to // get the midline and the text to line up reasonably well horizLine = (ctrl.row_height * row) + rowLeading + (int) (rowAscent*.75) - v_offset; if (plusBox == null || minusBox == null) { // buildBoxes will return false if our peer isn't // ready for us to create images if (!buildBoxes()) { return; } } x1 = x2 = leftSpacing + (minusBox.getWidth(this) / 2) - h_offset; bg.setColor(lineColor); if (s != null) { for (int i = 0; i < s.size(); i++) { x1 = x2; x2 = x1 + tabStep; if (s.elementAt(i) == SPACE) { continue; } // Draw our vertical line up to our upstairs neighbor if (row > 0) { bg.drawLine(x1, ctrl.row_height * row - v_offset, x1, horizLine); } // If appropriate, draw our vertical line down to where we'll meet // with our downstairs neighbor bg.drawLine(x1, horizLine, x1, ctrl.row_height * (row + 1) - v_offset); } } x1 = x2; x2 = x1 + tabStep; if (node.parent != null || node.prevSibling != null) { bg.drawLine(x1, ctrl.row_height * row - v_offset, x1, horizLine); } if (node.nextSibling != null) { bg.drawLine(x1, horizLine, x1, ctrl.row_height * (row + 1) - v_offset); } // Now draw from our connecting point over to where we'll draw our icon bg.drawLine(x1, horizLine, x2, horizLine); if (node.expandable || node.child != null) { if (node.expanded) { // draw our minus box over the intersection node.boxX1 = x1 - (minusBox.getWidth(this)/2); node.boxX2 = node.boxX1 + minusBox.getWidth(this); node.boxY1 = horizLine - (minusBox.getHeight(this)/2); node.boxY2 = node.boxY1 + minusBox.getHeight(this); bg.drawImage(minusBox, node.boxX1, node.boxY1, this); } else { // draw our plus box over the intersection node.boxX1 = x1 - (plusBox.getWidth(this)/2); node.boxX2 = node.boxX1 + plusBox.getWidth(this); node.boxY1 = horizLine - (plusBox.getHeight(this)/2); node.boxY2 = node.boxY1 + plusBox.getHeight(this); bg.drawImage(plusBox, node.boxX1, node.boxY1, this); } } x1 = x2; // (x1, horizLine) is the point we want to center our icon on // if we have an open child under us, draw down if (node.expanded && node.child != null) { bg.drawLine(x1, horizLine, x1, ctrl.row_height * (row + 1) - v_offset); } imageIndex = node.expanded ? node.openImage : node.closedImage; if (imageIndex >= 0 && imageIndex < images.length) { temp = images[imageIndex]; bg.drawImage(temp, x1 - (temp.getWidth(this)/2), horizLine - (temp.getHeight(this)/2), this); x1 = x1 + temp.getWidth(this)/2 + iconTextSpacing; } x2 = x1 + fontMetric.stringWidth(node.text); if (node.selected) { if (ctrl.isFocusOwner()) { bg.setColor(fgColor); } else { Color transparentFgColor = new Color(fgColor.getRed(), fgColor.getGreen(), fgColor.getBlue(), 64); bg.setColor(transparentFgColor); } bg.fillRect(x1, (horizLine - rowAscent/2), x2-x1+1, rowAscent + rowDescent + rowLeading + 1); bg.setColor(bgColor); } else if (spriteVisible && node == ctrl.dragNode) { bg.setColor(java.awt.Color.blue); bg.fillRect(x1, (horizLine - rowAscent/2), x2-x1+1, rowAscent + rowDescent + rowLeading + 1); bg.setColor(bgColor); } else { bg.setColor(fgColor); } bg.drawString(node.text, x1, horizLine + rowAscent/2); if (x2 > ctrl.maxWidth) { if (debug) { System.err.println("Width overrun in drawRow for node " + node); } } } /** * This method calculates the maximum width required for the tree in * its current state. */ int calcWidth() { int x, imageIndex; treeNode node; Vector drawVector; /* -- */ if (debug) { System.err.println("entering calcWidth()"); } ctrl.maxWidth = ctrl.minWidth; synchronized (ctrl.rows) { for (int i = 0; i < ctrl.rows.size(); i ++) { node = (treeNode) ctrl.rows.elementAt(i); if (debug && node.row != i) { System.err.println("ASSERT! node's row is " + node.row + ", not " + i); } if (node.parent == null) { drawVector = null; } else if (node.parent.childStack == null) { node.parent.childStack = new Stack(); recursiveGenStack(node.parent, node.parent.childStack); drawVector = node.parent.childStack; } else { drawVector = node.parent.childStack; } x = leftSpacing + (boxSize / 2) + tabStep; if (drawVector != null) { x = x + tabStep * drawVector.size(); } imageIndex = node.expanded ? node.openImage : node.closedImage; x = x + images[imageIndex].getWidth(this)/2 + iconTextSpacing + fontMetric.stringWidth(node.text); if (x > ctrl.maxWidth) { if (debug) { System.err.println("\nmax row width found at " + node.text); System.err.println("old width = " + ctrl.maxWidth); System.err.println("new width = " + x); System.err.println("----------"); } ctrl.maxWidth = x; } } } return ctrl.maxWidth; } /** * Generate our +/- box images */ private boolean buildBoxes() { Graphics g; /* -- */ int midpoint = boxSize/2; int p1 = 1; int p2 = boxSize - 1; plusBox = this.createImage(boxSize,boxSize); // it's possible we're being called before our peer is really // ready for us to create images.. if so, we'll just return // false and we'll try again at a later point if (plusBox == null) { return false; } g = plusBox.getGraphics(); // how do we know what color we're going to clear this to? g.setColor(bgColor); g.fillRect(0,0, boxSize, boxSize); g.setColor(Color.black); g.drawRect(p1, p1, p2-1, p2-1); g.setColor(Color.blue); g.drawLine(p1 + 2, midpoint, p2 - 2, midpoint); g.drawLine(midpoint, p1 + 2, midpoint, p2 - 2); minusBox = createImage(boxSize, boxSize); g = minusBox.getGraphics(); g.setColor(bgColor); g.fillRect(0,0, boxSize, boxSize); g.setColor(Color.black); g.drawRect(p1, p1, p2 - 1, p2 - 1); g.setColor(Color.blue); g.drawLine(p1 + 2, midpoint, p2 - 2, midpoint); return true; } // mouseListener methods public void mouseClicked(MouseEvent e) { treeNode node; int row; int x, y; /* -- */ ctrl.requestFocus(); x = e.getX(); y = e.getY(); row = (y + v_offset) / ctrl.row_height; try { node = (treeNode) ctrl.rows.elementAt(row); } catch (ArrayIndexOutOfBoundsException ex) { return; } if (debug2) { System.err.println("Clicked on node " + node.getText() + ", row " + row); System.err.println("Location: " + x + ", " + y); System.err.println("v_offset = " + v_offset); if (node.expandable) { System.err.println("Box for this row currently at (" + node.boxX1 + "," + node.boxY1 + "-" + node.boxX2 + "," + node.boxY2 + ")"); } } // if they double clicked outside of a box, open/close the node if (e.getClickCount() >= 2) { if ((node.expandable || node.child != null) && (x < node.boxX1 || y < node.boxY1 || x > node.boxX2 || y > node.boxY2)) { if (node.expanded) { ctrl.contractNode(node, true); } else { ctrl.expandNode(node, true); } return; } else if (!node.expandable) { ctrl.doubleClickNode(node); } } else { if (debug2) { System.err.println("Click in row " + row); } if ((node.expandable || node.child != null) && (x >= node.boxX1 && y >= node.boxY1 && x <= node.boxX2 && y <= node.boxY2)) { // mousePressed will have taken care of this for us. return; } } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { treeNode node; int row; int x, y; /* -- */ x = e.getX(); y = e.getY(); row = (y + v_offset) / ctrl.row_height; try { node = (treeNode) ctrl.rows.elementAt(row); } catch (ArrayIndexOutOfBoundsException ex) { // out of range, deselect all rows if (debug) { System.err.println("Hey! received out of bounds exception, row = " + row); } ctrl.unselectAllNodes(false); render(); repaint(); return; } // we don't want to take a right or middle mouse button click as anything // but a pop-up driver if (((e.getModifiers() & InputEvent.BUTTON2_MASK) != 0) || ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0)) { if (!spriteVisible) { ctrl.unselectAllNodes(true); // another node is being selected ctrl.selectNode(node); render(); repaint(); if (e.isPopupTrigger()) { popupHandler(e, node); } } return; } if ((node.expandable || node.child != null) && (x >= node.boxX1 && y >= node.boxY1 && x <= node.boxX2 && y <= node.boxY2)) { if (node.expanded) { ctrl.contractNode(node, true); } else { ctrl.expandNode(node, true); } return; } // remember that the mouse was last pressed on this node if // we subsequently receive mouseDragged events. if (ctrl.dragNode == null) { try { ctrl.dragNode = (treeNode) ctrl.rows.elementAt(row); if (debug2) { System.err.println("mousePressed(): I'm setting dragNode to " + ctrl.dragNode.getText() + "!!!"); } } catch (ArrayIndexOutOfBoundsException ex) { // ignore } } if (debug2) { System.err.println("Press in row " + row); } // we'll do the selection/popup logic here as well, just in case // we run on some weird system where the popup trigger is on // the main button without the CTRL or META modifiers (which // are equivalent to middle and right mouse buttons on AWT) if (!node.selected && !spriteVisible) { ctrl.unselectAllNodes(true); // another node is being selected ctrl.selectNode(node); render(); repaint(); } if (e.isPopupTrigger() && !spriteVisible) { popupHandler(e, node); return; } } public void mouseReleased(MouseEvent e) { treeNode node; int row; /* -- */ row = (e.getY() + v_offset) / ctrl.row_height; if (dragSelected && (ctrl.dragNode != ctrl.dragOverNode)) { ctrl.dCallback.iconDragDrop(ctrl.dragNode, ctrl.dragOverNode); } else if (drawLine && (ctrl.dragNode != ctrl.dragAboveNode) && (ctrl.dragNode != ctrl.dragBelowNode)) { ctrl.dCallback.dragLineRelease(ctrl.dragNode, ctrl.dragAboveNode, ctrl.dragBelowNode); } drawLine = false; dragSelected = false; if (spriteVisible) { spriteVisible = false; this.setCursor(Cursor.getDefaultCursor()); ctrl.refreshTree(); } dontdrag = false; ctrl.dragNode = null; if (debug2) { System.err.println("Released in row " + row); } try { node = (treeNode) ctrl.rows.elementAt(row); } catch (ArrayIndexOutOfBoundsException ex) { // out of range return; } if (node == null) { throw new RuntimeException("null node"); } // Win32 does popup trigger on mouse release if (e.isPopupTrigger() && !spriteVisible) { popupHandler(e, node); return; } } public void mouseDragged(MouseEvent e) { treeNode n = null; int row; boolean reRender = false; /* -- */ if (dontdrag || ctrl.dragNode == null) { return; } row = (e.getY() + v_offset) / ctrl.row_height; if (debug2) { System.err.println("Dragging over row " + row); } try { n = (treeNode) ctrl.rows.elementAt(row); } catch (ArrayIndexOutOfBoundsException ex) { // out of range.. go ahead and allow the // drag down below, but don't do any // further processing if (spriteVisible) { repaint(); } return; } if (debug2) { System.err.println("Dragging over Node: " + n.getText()); } // ** // Check for drag start // ** if (!spriteVisible && (ctrl.dragMode != treeControl.DRAG_NONE)) { if (ctrl.dCallback.startDrag(ctrl.dragNode)) { spriteVisible = true; if (debug2) { System.err.println("mouseDragged(): I'm setting dragNode to " + ctrl.dragNode.getText() + "!!!"); } this.setCursor(Cursor.getPredefinedCursor(java.awt.Cursor.CROSSHAIR_CURSOR)); sprite = images[n.closedImage]; spriteLoc = e.getPoint(); // System.err.println("Created sprite"); if ((ctrl.dragMode & ctrl.DRAG_ICON) != 0) { ctrl.oldNode = ctrl.dragNode; } } else { dontdrag = true; } } // if we are dragging, check out what we want to be doing if (spriteVisible) { spriteLoc = e.getPoint(); if (debug) { System.err.println("pos = " + spriteLoc.y); } // do drag mode specific processing if ((ctrl.dragMode & ctrl.DRAG_ICON) != 0) { // we only want to do the drag over test if we're not over // the same node we were last time if (!dragSelected || ctrl.oldNode != n) { if (ctrl.dCallback.iconDragOver(ctrl.dragNode, n)) { if (debug2) { System.err.println("** treeControl: iconDragOver <" + n.getText() + "> returned true"); } dragSelected = true; ctrl.dragOverNode = n; drawLine = false; ctrl.transientSelectNode(n); reRender = true; } else { if (debug2) { System.err.println("** treeControl: iconDragOver <" + n.getText() + "> returned false"); } dragSelected = false; ctrl.dragOverNode = null; ctrl.transientUnselectNode(n); } if ((ctrl.oldNode != n) && (ctrl.oldNode != null)) { ctrl.transientUnselectNode(ctrl.oldNode); } ctrl.oldNode = n; } else { if (dragSelected) { if (debug2) { System.err.println("** still dragging over selected node " + n.getText()); } drawLine = false; } else { if (debug2) { System.err.println("** still dragging over unselected node " + n.getText()); } } } } // okay, we've done determinations on dragging the icon onto a row // if necessary.. now check to see if we are also in a position // to place a line between rows.. if we are, we'll essentially forget // about the dragOver if we're not near the center of the row. if ((ctrl.dragMode & ctrl.DRAG_LINE) != 0) { treeNode aboveNode, belowNode; /* -- */ // if the mouse is below the midline of row n, n will be above the // line we're calculating if (spriteLoc.y > (n.row * ctrl.row_height + (ctrl.row_height / 2) - v_offset)) { aboveNode = n; if (debug2) { System.err.println("aboveNode is " + aboveNode.getText()); } try { belowNode = (treeNode) ctrl.rows.elementAt(aboveNode.row + 1); if (debug2) { System.err.println("belowNode is " + belowNode.getText()); } } catch (ArrayIndexOutOfBoundsException ex) { belowNode = null; } } else { belowNode = n; if (debug2) { System.err.println("belowNode is " + belowNode.getText()); } try { aboveNode = (treeNode) ctrl.rows.elementAt(belowNode.row - 1); if (debug2) { System.err.println("aboveNode is " + aboveNode.getText()); } } catch (ArrayIndexOutOfBoundsException ex) { aboveNode = null; } } // if we've moved into a new region, update the ctrl's notion // of our position if ((aboveNode != ctrl.dragAboveNode) || (belowNode != ctrl.dragBelowNode)) { if (debug) { System.err.println("Setting hiBound/loBound"); } hiBound = n.row * ctrl.row_height + (ctrl.row_height / 6) - v_offset; loBound = (n.row + 1) * ctrl.row_height - (ctrl.row_height / 6) - v_offset; ctrl.dragAboveNode = aboveNode; ctrl.dragBelowNode = belowNode; } if (debug) { System.err.println("hiBound for node " + n.getText() + " is " + hiBound); System.err.println("loBound for node " + n.getText() + " is " + loBound); System.err.println("(spriteLoc.y == " + spriteLoc.y + ")"); } if (!dragSelected || ((spriteLoc.y < hiBound) || (spriteLoc.y > loBound))) { if (debug2) { if (dragSelected) { System.err.println("dragSelected is true.. mixed mode check"); } else { System.err.println("dragSelected is false.. just tweening"); } System.err.println("Checking for dragLineTween"); } if (ctrl.dCallback.dragLineTween(ctrl.dragNode, aboveNode, belowNode)) { if (debug2) { System.err.println("dragLineTween affirm"); } drawLine = true; dragSelected = false; if (ctrl.dragOverNode != null) { ctrl.transientUnselectNode(ctrl.dragOverNode); } reRender = true; } else { drawLine = false; } } } else { drawLine = false; } } if (reRender) { if (debug) { System.err.println("treeControl: ** Rendering"); } render(); } if (reRender || spriteVisible) { repaint(); } } public void mouseMoved(MouseEvent e) { } /** * popup dispatcher */ void popupHandler(MouseEvent e, treeNode node) { if (debug2) { System.err.println("popupHandler"); } ctrl.menuedNode = node; if (node.menu != null) { if (debug2) { System.err.println("node popup"); } node.menu.show(this, e.getX(), e.getY()); } else if (ctrl.menu != null) { if (debug2) { System.err.println("ctrl popup"); } ctrl.menu.show(this, e.getX(), e.getY()); } } /** * Method to be called after this treeCanvas is removed from use and * visibility. */ public synchronized void dispose() { if (bg != null) { bg.dispose(); bg = null; } if (plusBox != null) { plusBox.flush(); plusBox = null; } if (minusBox != null) { minusBox.flush(); minusBox = null; } if (backing != null) { backing.flush(); backing = null; } if (images != null) { for (int i = 0; i < images.length; i++) { if (images[i] != null) { images[i].flush(); images[i] = null; } } } if (sprite != null) { sprite.flush(); sprite = null; } } } /*------------------------------------------------------------------------------ class Range ------------------------------------------------------------------------------*/ /** * This class is used as a simple struct to hold scratch information * for the {@link arlut.csd.JTree.treeControl treeControl} rendering * logic. */ class Range { int low; int high; Range(int low, int high) { this.low = low; this.high = high; } }