| /* BasicTreeUI.java -- |
| Copyright (C) 2002, 2004, 2005, 2006, Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath 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, or (at your option) |
| any later version. |
| |
| GNU Classpath 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 GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package javax.swing.plaf.basic; |
| |
| import gnu.classpath.NotImplementedException; |
| import gnu.javax.swing.tree.GnuPath; |
| |
| import java.awt.Color; |
| import java.awt.Component; |
| import java.awt.Dimension; |
| import java.awt.Font; |
| import java.awt.FontMetrics; |
| import java.awt.Graphics; |
| import java.awt.Insets; |
| import java.awt.Label; |
| import java.awt.Rectangle; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.awt.event.ComponentAdapter; |
| import java.awt.event.ComponentEvent; |
| import java.awt.event.ComponentListener; |
| import java.awt.event.FocusEvent; |
| import java.awt.event.FocusListener; |
| import java.awt.event.InputEvent; |
| import java.awt.event.KeyAdapter; |
| import java.awt.event.KeyEvent; |
| import java.awt.event.KeyListener; |
| import java.awt.event.MouseAdapter; |
| import java.awt.event.MouseEvent; |
| import java.awt.event.MouseListener; |
| import java.awt.event.MouseMotionListener; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.util.Enumeration; |
| import java.util.Hashtable; |
| |
| import javax.swing.AbstractAction; |
| import javax.swing.Action; |
| import javax.swing.ActionMap; |
| import javax.swing.CellRendererPane; |
| import javax.swing.Icon; |
| import javax.swing.InputMap; |
| import javax.swing.JComponent; |
| import javax.swing.JScrollBar; |
| import javax.swing.JScrollPane; |
| import javax.swing.JTree; |
| import javax.swing.LookAndFeel; |
| import javax.swing.SwingUtilities; |
| import javax.swing.Timer; |
| import javax.swing.UIManager; |
| import javax.swing.event.CellEditorListener; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.MouseInputListener; |
| import javax.swing.event.TreeExpansionEvent; |
| import javax.swing.event.TreeExpansionListener; |
| import javax.swing.event.TreeModelEvent; |
| import javax.swing.event.TreeModelListener; |
| import javax.swing.event.TreeSelectionEvent; |
| import javax.swing.event.TreeSelectionListener; |
| import javax.swing.plaf.ActionMapUIResource; |
| import javax.swing.plaf.ComponentUI; |
| import javax.swing.plaf.TreeUI; |
| import javax.swing.tree.AbstractLayoutCache; |
| import javax.swing.tree.DefaultTreeCellEditor; |
| import javax.swing.tree.DefaultTreeCellRenderer; |
| import javax.swing.tree.TreeCellEditor; |
| import javax.swing.tree.TreeCellRenderer; |
| import javax.swing.tree.TreeModel; |
| import javax.swing.tree.TreeNode; |
| import javax.swing.tree.TreePath; |
| import javax.swing.tree.TreeSelectionModel; |
| import javax.swing.tree.VariableHeightLayoutCache; |
| |
| /** |
| * A delegate providing the user interface for <code>JTree</code> according to |
| * the Basic look and feel. |
| * |
| * @see javax.swing.JTree |
| * @author Lillian Angel (langel@redhat.com) |
| * @author Sascha Brawer (brawer@dandelis.ch) |
| * @author Audrius Meskauskas (audriusa@bioinformatics.org) |
| */ |
| public class BasicTreeUI |
| extends TreeUI |
| { |
| /** |
| * The tree cell editing may be started by the single mouse click on the |
| * selected cell. To separate it from the double mouse click, the editing |
| * session starts after this time (in ms) after that single click, and only no |
| * other clicks were performed during that time. |
| */ |
| static int WAIT_TILL_EDITING = 900; |
| |
| /** Collapse Icon for the tree. */ |
| protected transient Icon collapsedIcon; |
| |
| /** Expanded Icon for the tree. */ |
| protected transient Icon expandedIcon; |
| |
| /** Distance between left margin and where vertical dashes will be drawn. */ |
| protected int leftChildIndent; |
| |
| /** |
| * Distance between leftChildIndent and where cell contents will be drawn. |
| */ |
| protected int rightChildIndent; |
| |
| /** |
| * Total fistance that will be indented. The sum of leftChildIndent and |
| * rightChildIndent . |
| */ |
| protected int totalChildIndent; |
| |
| /** Index of the row that was last selected. */ |
| protected int lastSelectedRow; |
| |
| /** Component that we're going to be drawing onto. */ |
| protected JTree tree; |
| |
| /** Renderer that is being used to do the actual cell drawing. */ |
| protected transient TreeCellRenderer currentCellRenderer; |
| |
| /** |
| * Set to true if the renderer that is currently in the tree was created by |
| * this instance. |
| */ |
| protected boolean createdRenderer; |
| |
| /** Editor for the tree. */ |
| protected transient TreeCellEditor cellEditor; |
| |
| /** |
| * Set to true if editor that is currently in the tree was created by this |
| * instance. |
| */ |
| protected boolean createdCellEditor; |
| |
| /** |
| * Set to false when editing and shouldSelectCall() returns true meaning the |
| * node should be selected before editing, used in completeEditing. |
| * GNU Classpath editing is implemented differently, so this value is not |
| * actually read anywhere. However it is always set correctly to maintain |
| * interoperability with the derived classes that read this field. |
| */ |
| protected boolean stopEditingInCompleteEditing; |
| |
| /** Used to paint the TreeCellRenderer. */ |
| protected CellRendererPane rendererPane; |
| |
| /** Size needed to completely display all the nodes. */ |
| protected Dimension preferredSize; |
| |
| /** Minimum size needed to completely display all the nodes. */ |
| protected Dimension preferredMinSize; |
| |
| /** Is the preferredSize valid? */ |
| protected boolean validCachedPreferredSize; |
| |
| /** Object responsible for handling sizing and expanded issues. */ |
| protected AbstractLayoutCache treeState; |
| |
| /** Used for minimizing the drawing of vertical lines. */ |
| protected Hashtable drawingCache; |
| |
| /** |
| * True if doing optimizations for a largeModel. Subclasses that don't support |
| * this may wish to override createLayoutCache to not return a |
| * FixedHeightLayoutCache instance. |
| */ |
| protected boolean largeModel; |
| |
| /** Responsible for telling the TreeState the size needed for a node. */ |
| protected AbstractLayoutCache.NodeDimensions nodeDimensions; |
| |
| /** Used to determine what to display. */ |
| protected TreeModel treeModel; |
| |
| /** Model maintaining the selection. */ |
| protected TreeSelectionModel treeSelectionModel; |
| |
| /** |
| * How much the depth should be offset to properly calculate x locations. This |
| * is based on whether or not the root is visible, and if the root handles are |
| * visible. |
| */ |
| protected int depthOffset; |
| |
| /** |
| * When editing, this will be the Component that is doing the actual editing. |
| */ |
| protected Component editingComponent; |
| |
| /** Path that is being edited. */ |
| protected TreePath editingPath; |
| |
| /** |
| * Row that is being edited. Should only be referenced if editingComponent is |
| * null. |
| */ |
| protected int editingRow; |
| |
| /** Set to true if the editor has a different size than the renderer. */ |
| protected boolean editorHasDifferentSize; |
| |
| /** Boolean to keep track of editing. */ |
| boolean isEditing; |
| |
| /** The current path of the visible nodes in the tree. */ |
| TreePath currentVisiblePath; |
| |
| /** The gap between the icon and text. */ |
| int gap = 4; |
| |
| /** The max height of the nodes in the tree. */ |
| int maxHeight; |
| |
| /** The hash color. */ |
| Color hashColor; |
| |
| /** Listeners */ |
| PropertyChangeListener propertyChangeListener; |
| |
| FocusListener focusListener; |
| |
| TreeSelectionListener treeSelectionListener; |
| |
| MouseListener mouseListener; |
| |
| KeyListener keyListener; |
| |
| PropertyChangeListener selectionModelPropertyChangeListener; |
| |
| ComponentListener componentListener; |
| |
| CellEditorListener cellEditorListener; |
| |
| TreeExpansionListener treeExpansionListener; |
| |
| TreeModelListener treeModelListener; |
| |
| /** |
| * This timer fires the editing action after about 1200 ms if not reset during |
| * that time. It handles the editing start with the single mouse click (and |
| * not the double mouse click) on the selected tree node. |
| */ |
| Timer startEditTimer; |
| |
| /** |
| * The zero size icon, used for expand controls, if they are not visible. |
| */ |
| static Icon nullIcon; |
| |
| /** |
| * The special value of the mouse event is sent indicating that this is not |
| * just the mouse click, but the mouse click on the selected node. Sending |
| * such event forces to start the cell editing session. |
| */ |
| static final MouseEvent EDIT = new MouseEvent(new Label(), 7, 7, 7, 7, 7, 7, |
| false); |
| |
| /** |
| * Creates a new BasicTreeUI object. |
| */ |
| public BasicTreeUI() |
| { |
| validCachedPreferredSize = false; |
| drawingCache = new Hashtable(); |
| nodeDimensions = createNodeDimensions(); |
| configureLayoutCache(); |
| |
| editingRow = - 1; |
| lastSelectedRow = - 1; |
| } |
| |
| /** |
| * Returns an instance of the UI delegate for the specified component. |
| * |
| * @param c the <code>JComponent</code> for which we need a UI delegate for. |
| * @return the <code>ComponentUI</code> for c. |
| */ |
| public static ComponentUI createUI(JComponent c) |
| { |
| return new BasicTreeUI(); |
| } |
| |
| /** |
| * Returns the Hash color. |
| * |
| * @return the <code>Color</code> of the Hash. |
| */ |
| protected Color getHashColor() |
| { |
| return hashColor; |
| } |
| |
| /** |
| * Sets the Hash color. |
| * |
| * @param color the <code>Color</code> to set the Hash to. |
| */ |
| protected void setHashColor(Color color) |
| { |
| hashColor = color; |
| } |
| |
| /** |
| * Sets the left child's indent value. |
| * |
| * @param newAmount is the new indent value for the left child. |
| */ |
| public void setLeftChildIndent(int newAmount) |
| { |
| leftChildIndent = newAmount; |
| } |
| |
| /** |
| * Returns the indent value for the left child. |
| * |
| * @return the indent value for the left child. |
| */ |
| public int getLeftChildIndent() |
| { |
| return leftChildIndent; |
| } |
| |
| /** |
| * Sets the right child's indent value. |
| * |
| * @param newAmount is the new indent value for the right child. |
| */ |
| public void setRightChildIndent(int newAmount) |
| { |
| rightChildIndent = newAmount; |
| } |
| |
| /** |
| * Returns the indent value for the right child. |
| * |
| * @return the indent value for the right child. |
| */ |
| public int getRightChildIndent() |
| { |
| return rightChildIndent; |
| } |
| |
| /** |
| * Sets the expanded icon. |
| * |
| * @param newG is the new expanded icon. |
| */ |
| public void setExpandedIcon(Icon newG) |
| { |
| expandedIcon = newG; |
| } |
| |
| /** |
| * Returns the current expanded icon. |
| * |
| * @return the current expanded icon. |
| */ |
| public Icon getExpandedIcon() |
| { |
| return expandedIcon; |
| } |
| |
| /** |
| * Sets the collapsed icon. |
| * |
| * @param newG is the new collapsed icon. |
| */ |
| public void setCollapsedIcon(Icon newG) |
| { |
| collapsedIcon = newG; |
| } |
| |
| /** |
| * Returns the current collapsed icon. |
| * |
| * @return the current collapsed icon. |
| */ |
| public Icon getCollapsedIcon() |
| { |
| return collapsedIcon; |
| } |
| |
| /** |
| * Updates the componentListener, if necessary. |
| * |
| * @param largeModel sets this.largeModel to it. |
| */ |
| protected void setLargeModel(boolean largeModel) |
| { |
| if (largeModel != this.largeModel) |
| { |
| tree.removeComponentListener(componentListener); |
| this.largeModel = largeModel; |
| tree.addComponentListener(componentListener); |
| } |
| } |
| |
| /** |
| * Returns true if largeModel is set |
| * |
| * @return true if largeModel is set, otherwise false. |
| */ |
| protected boolean isLargeModel() |
| { |
| return largeModel; |
| } |
| |
| /** |
| * Sets the row height. |
| * |
| * @param rowHeight is the height to set this.rowHeight to. |
| */ |
| protected void setRowHeight(int rowHeight) |
| { |
| if (rowHeight == 0) |
| rowHeight = getMaxHeight(tree); |
| treeState.setRowHeight(rowHeight); |
| } |
| |
| /** |
| * Returns the current row height. |
| * |
| * @return current row height. |
| */ |
| protected int getRowHeight() |
| { |
| return tree.getRowHeight(); |
| } |
| |
| /** |
| * Sets the TreeCellRenderer to <code>tcr</code>. This invokes |
| * <code>updateRenderer</code>. |
| * |
| * @param tcr is the new TreeCellRenderer. |
| */ |
| protected void setCellRenderer(TreeCellRenderer tcr) |
| { |
| // Finish editing before changing the renderer. |
| completeEditing(); |
| |
| // The renderer is set in updateRenderer. |
| updateRenderer(); |
| |
| // Refresh the layout if necessary. |
| if (treeState != null) |
| { |
| treeState.invalidateSizes(); |
| updateSize(); |
| } |
| } |
| |
| /** |
| * Return currentCellRenderer, which will either be the trees renderer, or |
| * defaultCellRenderer, which ever was not null. |
| * |
| * @return the current Cell Renderer |
| */ |
| protected TreeCellRenderer getCellRenderer() |
| { |
| if (currentCellRenderer != null) |
| return currentCellRenderer; |
| |
| return createDefaultCellRenderer(); |
| } |
| |
| /** |
| * Sets the tree's model. |
| * |
| * @param model to set the treeModel to. |
| */ |
| protected void setModel(TreeModel model) |
| { |
| completeEditing(); |
| |
| if (treeModel != null && treeModelListener != null) |
| treeModel.removeTreeModelListener(treeModelListener); |
| |
| treeModel = tree.getModel(); |
| |
| if (treeModel != null && treeModelListener != null) |
| treeModel.addTreeModelListener(treeModelListener); |
| |
| if (treeState != null) |
| { |
| treeState.setModel(treeModel); |
| updateLayoutCacheExpandedNodes(); |
| updateSize(); |
| } |
| } |
| |
| /** |
| * Returns the tree's model |
| * |
| * @return treeModel |
| */ |
| protected TreeModel getModel() |
| { |
| return treeModel; |
| } |
| |
| /** |
| * Sets the root to being visible. |
| * |
| * @param newValue sets the visibility of the root |
| */ |
| protected void setRootVisible(boolean newValue) |
| { |
| tree.setRootVisible(newValue); |
| } |
| |
| /** |
| * Returns true if the root is visible. |
| * |
| * @return true if the root is visible. |
| */ |
| protected boolean isRootVisible() |
| { |
| return tree.isRootVisible(); |
| } |
| |
| /** |
| * Determines whether the node handles are to be displayed. |
| * |
| * @param newValue sets whether or not node handles should be displayed. |
| */ |
| protected void setShowsRootHandles(boolean newValue) |
| { |
| completeEditing(); |
| updateDepthOffset(); |
| if (treeState != null) |
| { |
| treeState.invalidateSizes(); |
| updateSize(); |
| } |
| } |
| |
| /** |
| * Returns true if the node handles are to be displayed. |
| * |
| * @return true if the node handles are to be displayed. |
| */ |
| protected boolean getShowsRootHandles() |
| { |
| return tree.getShowsRootHandles(); |
| } |
| |
| /** |
| * Sets the cell editor. |
| * |
| * @param editor to set the cellEditor to. |
| */ |
| protected void setCellEditor(TreeCellEditor editor) |
| { |
| cellEditor = editor; |
| createdCellEditor = true; |
| } |
| |
| /** |
| * Returns the <code>TreeCellEditor</code> for this tree. |
| * |
| * @return the cellEditor for this tree. |
| */ |
| protected TreeCellEditor getCellEditor() |
| { |
| return cellEditor; |
| } |
| |
| /** |
| * Configures the receiver to allow, or not allow, editing. |
| * |
| * @param newValue sets the receiver to allow editing if true. |
| */ |
| protected void setEditable(boolean newValue) |
| { |
| tree.setEditable(newValue); |
| } |
| |
| /** |
| * Returns true if the receiver allows editing. |
| * |
| * @return true if the receiver allows editing. |
| */ |
| protected boolean isEditable() |
| { |
| return tree.isEditable(); |
| } |
| |
| /** |
| * Resets the selection model. The appropriate listeners are installed on the |
| * model. |
| * |
| * @param newLSM resets the selection model. |
| */ |
| protected void setSelectionModel(TreeSelectionModel newLSM) |
| { |
| if (newLSM != null) |
| { |
| treeSelectionModel = newLSM; |
| tree.setSelectionModel(treeSelectionModel); |
| } |
| } |
| |
| /** |
| * Returns the current selection model. |
| * |
| * @return the current selection model. |
| */ |
| protected TreeSelectionModel getSelectionModel() |
| { |
| return treeSelectionModel; |
| } |
| |
| /** |
| * Returns the Rectangle enclosing the label portion that the last item in |
| * path will be drawn to. Will return null if any component in path is |
| * currently valid. |
| * |
| * @param tree is the current tree the path will be drawn to. |
| * @param path is the current path the tree to draw to. |
| * @return the Rectangle enclosing the label portion that the last item in the |
| * path will be drawn to. |
| */ |
| public Rectangle getPathBounds(JTree tree, TreePath path) |
| { |
| return treeState.getBounds(path, new Rectangle()); |
| } |
| |
| /** |
| * Returns the max height of all the nodes in the tree. |
| * |
| * @param tree - the current tree |
| * @return the max height. |
| */ |
| int getMaxHeight(JTree tree) |
| { |
| if (maxHeight != 0) |
| return maxHeight; |
| |
| Icon e = UIManager.getIcon("Tree.openIcon"); |
| Icon c = UIManager.getIcon("Tree.closedIcon"); |
| Icon l = UIManager.getIcon("Tree.leafIcon"); |
| int rc = getRowCount(tree); |
| int iconHeight = 0; |
| |
| for (int row = 0; row < rc; row++) |
| { |
| if (isLeaf(row)) |
| iconHeight = l.getIconHeight(); |
| else if (tree.isExpanded(row)) |
| iconHeight = e.getIconHeight(); |
| else |
| iconHeight = c.getIconHeight(); |
| |
| maxHeight = Math.max(maxHeight, iconHeight + gap); |
| } |
| |
| treeState.setRowHeight(maxHeight); |
| return maxHeight; |
| } |
| |
| /** |
| * Get the tree node icon. |
| */ |
| Icon getNodeIcon(TreePath path) |
| { |
| Object node = path.getLastPathComponent(); |
| if (treeModel.isLeaf(node)) |
| return UIManager.getIcon("Tree.leafIcon"); |
| else if (treeState.getExpandedState(path)) |
| return UIManager.getIcon("Tree.openIcon"); |
| else |
| return UIManager.getIcon("Tree.closedIcon"); |
| } |
| |
| /** |
| * Returns the path for passed in row. If row is not visible null is returned. |
| * |
| * @param tree is the current tree to return path for. |
| * @param row is the row number of the row to return. |
| * @return the path for passed in row. If row is not visible null is returned. |
| */ |
| public TreePath getPathForRow(JTree tree, int row) |
| { |
| return treeState.getPathForRow(row); |
| } |
| |
| /** |
| * Returns the row that the last item identified in path is visible at. Will |
| * return -1 if any of the elments in the path are not currently visible. |
| * |
| * @param tree is the current tree to return the row for. |
| * @param path is the path used to find the row. |
| * @return the row that the last item identified in path is visible at. Will |
| * return -1 if any of the elments in the path are not currently |
| * visible. |
| */ |
| public int getRowForPath(JTree tree, TreePath path) |
| { |
| return treeState.getRowForPath(path); |
| } |
| |
| /** |
| * Returns the number of rows that are being displayed. |
| * |
| * @param tree is the current tree to return the number of rows for. |
| * @return the number of rows being displayed. |
| */ |
| public int getRowCount(JTree tree) |
| { |
| return treeState.getRowCount(); |
| } |
| |
| /** |
| * Returns the path to the node that is closest to x,y. If there is nothing |
| * currently visible this will return null, otherwise it'll always return a |
| * valid path. If you need to test if the returned object is exactly at x,y |
| * you should get the bounds for the returned path and test x,y against that. |
| * |
| * @param tree the tree to search for the closest path |
| * @param x is the x coordinate of the location to search |
| * @param y is the y coordinate of the location to search |
| * @return the tree path closes to x,y. |
| */ |
| public TreePath getClosestPathForLocation(JTree tree, int x, int y) |
| { |
| return treeState.getPathClosestTo(x, y); |
| } |
| |
| /** |
| * Returns true if the tree is being edited. The item that is being edited can |
| * be returned by getEditingPath(). |
| * |
| * @param tree is the tree to check for editing. |
| * @return true if the tree is being edited. |
| */ |
| public boolean isEditing(JTree tree) |
| { |
| return isEditing; |
| } |
| |
| /** |
| * Stops the current editing session. This has no effect if the tree is not |
| * being edited. Returns true if the editor allows the editing session to |
| * stop. |
| * |
| * @param tree is the tree to stop the editing on |
| * @return true if the editor allows the editing session to stop. |
| */ |
| public boolean stopEditing(JTree tree) |
| { |
| if (isEditing(tree)) |
| { |
| completeEditing(false, false, true); |
| finish(); |
| } |
| return ! isEditing(tree); |
| } |
| |
| /** |
| * Cancels the current editing session. |
| * |
| * @param tree is the tree to cancel the editing session on. |
| */ |
| public void cancelEditing(JTree tree) |
| { |
| // There is no need to send the cancel message to the editor, |
| // as the cancellation event itself arrives from it. This would |
| // only be necessary when cancelling the editing programatically. |
| completeEditing(false, false, false); |
| finish(); |
| } |
| |
| /** |
| * Selects the last item in path and tries to edit it. Editing will fail if |
| * the CellEditor won't allow it for the selected item. |
| * |
| * @param tree is the tree to edit on. |
| * @param path is the path in tree to edit on. |
| */ |
| public void startEditingAtPath(JTree tree, TreePath path) |
| { |
| startEditing(path, null); |
| } |
| |
| /** |
| * Returns the path to the element that is being editted. |
| * |
| * @param tree is the tree to get the editing path from. |
| * @return the path that is being edited. |
| */ |
| public TreePath getEditingPath(JTree tree) |
| { |
| return editingPath; |
| } |
| |
| /** |
| * Invoked after the tree instance variable has been set, but before any |
| * default/listeners have been installed. |
| */ |
| protected void prepareForUIInstall() |
| { |
| lastSelectedRow = -1; |
| preferredSize = new Dimension(); |
| largeModel = tree.isLargeModel(); |
| preferredSize = new Dimension(); |
| setModel(tree.getModel()); |
| } |
| |
| /** |
| * Invoked from installUI after all the defaults/listeners have been |
| * installed. |
| */ |
| protected void completeUIInstall() |
| { |
| setShowsRootHandles(tree.getShowsRootHandles()); |
| updateRenderer(); |
| updateDepthOffset(); |
| setSelectionModel(tree.getSelectionModel()); |
| configureLayoutCache(); |
| treeState.setRootVisible(tree.isRootVisible()); |
| treeSelectionModel.setRowMapper(treeState); |
| updateSize(); |
| } |
| |
| /** |
| * Invoked from uninstallUI after all the defaults/listeners have been |
| * uninstalled. |
| */ |
| protected void completeUIUninstall() |
| { |
| tree = null; |
| } |
| |
| /** |
| * Installs the subcomponents of the tree, which is the renderer pane. |
| */ |
| protected void installComponents() |
| { |
| currentCellRenderer = createDefaultCellRenderer(); |
| rendererPane = createCellRendererPane(); |
| createdRenderer = true; |
| setCellRenderer(currentCellRenderer); |
| } |
| |
| /** |
| * Creates an instance of NodeDimensions that is able to determine the size of |
| * a given node in the tree. The node dimensions must be created before |
| * configuring the layout cache. |
| * |
| * @return the NodeDimensions of a given node in the tree |
| */ |
| protected AbstractLayoutCache.NodeDimensions createNodeDimensions() |
| { |
| return new NodeDimensionsHandler(); |
| } |
| |
| /** |
| * Creates a listener that is reponsible for the updates the UI based on how |
| * the tree changes. |
| * |
| * @return the PropertyChangeListener that is reposnsible for the updates |
| */ |
| protected PropertyChangeListener createPropertyChangeListener() |
| { |
| return new PropertyChangeHandler(); |
| } |
| |
| /** |
| * Creates the listener responsible for updating the selection based on mouse |
| * events. |
| * |
| * @return the MouseListener responsible for updating. |
| */ |
| protected MouseListener createMouseListener() |
| { |
| return new MouseHandler(); |
| } |
| |
| /** |
| * Creates the listener that is responsible for updating the display when |
| * focus is lost/grained. |
| * |
| * @return the FocusListener responsible for updating. |
| */ |
| protected FocusListener createFocusListener() |
| { |
| return new FocusHandler(); |
| } |
| |
| /** |
| * Creates the listener reponsible for getting key events from the tree. |
| * |
| * @return the KeyListener responsible for getting key events. |
| */ |
| protected KeyListener createKeyListener() |
| { |
| return new KeyHandler(); |
| } |
| |
| /** |
| * Creates the listener responsible for getting property change events from |
| * the selection model. |
| * |
| * @returns the PropertyChangeListener reponsible for getting property change |
| * events from the selection model. |
| */ |
| protected PropertyChangeListener createSelectionModelPropertyChangeListener() |
| { |
| return new SelectionModelPropertyChangeHandler(); |
| } |
| |
| /** |
| * Creates the listener that updates the display based on selection change |
| * methods. |
| * |
| * @return the TreeSelectionListener responsible for updating. |
| */ |
| protected TreeSelectionListener createTreeSelectionListener() |
| { |
| return new TreeSelectionHandler(); |
| } |
| |
| /** |
| * Creates a listener to handle events from the current editor |
| * |
| * @return the CellEditorListener that handles events from the current editor |
| */ |
| protected CellEditorListener createCellEditorListener() |
| { |
| return new CellEditorHandler(); |
| } |
| |
| /** |
| * Creates and returns a new ComponentHandler. This is used for the large |
| * model to mark the validCachedPreferredSize as invalid when the component |
| * moves. |
| * |
| * @return a new ComponentHandler. |
| */ |
| protected ComponentListener createComponentListener() |
| { |
| return new ComponentHandler(); |
| } |
| |
| /** |
| * Creates and returns the object responsible for updating the treestate when |
| * a nodes expanded state changes. |
| * |
| * @return the TreeExpansionListener responsible for updating the treestate |
| */ |
| protected TreeExpansionListener createTreeExpansionListener() |
| { |
| return new TreeExpansionHandler(); |
| } |
| |
| /** |
| * Creates the object responsible for managing what is expanded, as well as |
| * the size of nodes. |
| * |
| * @return the object responsible for managing what is expanded. |
| */ |
| protected AbstractLayoutCache createLayoutCache() |
| { |
| return new VariableHeightLayoutCache(); |
| } |
| |
| /** |
| * Returns the renderer pane that renderer components are placed in. |
| * |
| * @return the rendererpane that render components are placed in. |
| */ |
| protected CellRendererPane createCellRendererPane() |
| { |
| return new CellRendererPane(); |
| } |
| |
| /** |
| * Creates a default cell editor. |
| * |
| * @return the default cell editor. |
| */ |
| protected TreeCellEditor createDefaultCellEditor() |
| { |
| DefaultTreeCellEditor ed; |
| if (currentCellRenderer != null |
| && currentCellRenderer instanceof DefaultTreeCellRenderer) |
| ed = new DefaultTreeCellEditor(tree, |
| (DefaultTreeCellRenderer) currentCellRenderer); |
| else |
| ed = new DefaultTreeCellEditor(tree, null); |
| return ed; |
| } |
| |
| /** |
| * Returns the default cell renderer that is used to do the stamping of each |
| * node. |
| * |
| * @return the default cell renderer that is used to do the stamping of each |
| * node. |
| */ |
| protected TreeCellRenderer createDefaultCellRenderer() |
| { |
| return new DefaultTreeCellRenderer(); |
| } |
| |
| /** |
| * Returns a listener that can update the tree when the model changes. |
| * |
| * @return a listener that can update the tree when the model changes. |
| */ |
| protected TreeModelListener createTreeModelListener() |
| { |
| return new TreeModelHandler(); |
| } |
| |
| /** |
| * Uninstall all registered listeners |
| */ |
| protected void uninstallListeners() |
| { |
| tree.removePropertyChangeListener(propertyChangeListener); |
| tree.removeFocusListener(focusListener); |
| tree.removeTreeSelectionListener(treeSelectionListener); |
| tree.removeMouseListener(mouseListener); |
| tree.removeKeyListener(keyListener); |
| tree.removePropertyChangeListener(selectionModelPropertyChangeListener); |
| tree.removeComponentListener(componentListener); |
| tree.removeTreeExpansionListener(treeExpansionListener); |
| |
| TreeCellEditor tce = tree.getCellEditor(); |
| if (tce != null) |
| tce.removeCellEditorListener(cellEditorListener); |
| if (treeModel != null) |
| treeModel.removeTreeModelListener(treeModelListener); |
| } |
| |
| /** |
| * Uninstall all keyboard actions. |
| */ |
| protected void uninstallKeyboardActions() |
| { |
| tree.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).setParent( |
| null); |
| tree.getActionMap().setParent(null); |
| } |
| |
| /** |
| * Uninstall the rendererPane. |
| */ |
| protected void uninstallComponents() |
| { |
| currentCellRenderer = null; |
| rendererPane = null; |
| createdRenderer = false; |
| setCellRenderer(currentCellRenderer); |
| } |
| |
| /** |
| * The vertical element of legs between nodes starts at the bottom of the |
| * parent node by default. This method makes the leg start below that. |
| * |
| * @return the vertical leg buffer |
| */ |
| protected int getVerticalLegBuffer() |
| { |
| return getRowHeight() / 2; |
| } |
| |
| /** |
| * The horizontal element of legs between nodes starts at the right of the |
| * left-hand side of the child node by default. This method makes the leg end |
| * before that. |
| * |
| * @return the horizontal leg buffer |
| */ |
| protected int getHorizontalLegBuffer() |
| { |
| return rightChildIndent / 2; |
| } |
| |
| /** |
| * Make all the nodes that are expanded in JTree expanded in LayoutCache. This |
| * invokes updateExpandedDescendants with the root path. |
| */ |
| protected void updateLayoutCacheExpandedNodes() |
| { |
| if (treeModel != null && treeModel.getRoot() != null) |
| updateExpandedDescendants(new TreePath(treeModel.getRoot())); |
| } |
| |
| /** |
| * Updates the expanded state of all the descendants of the <code>path</code> |
| * by getting the expanded descendants from the tree and forwarding to the |
| * tree state. |
| * |
| * @param path the path used to update the expanded states |
| */ |
| protected void updateExpandedDescendants(TreePath path) |
| { |
| Enumeration expanded = tree.getExpandedDescendants(path); |
| while (expanded.hasMoreElements()) |
| treeState.setExpandedState((TreePath) expanded.nextElement(), true); |
| } |
| |
| /** |
| * Returns a path to the last child of <code>parent</code> |
| * |
| * @param parent is the topmost path to specified |
| * @return a path to the last child of parent |
| */ |
| protected TreePath getLastChildPath(TreePath parent) |
| { |
| return (TreePath) parent.getLastPathComponent(); |
| } |
| |
| /** |
| * Updates how much each depth should be offset by. |
| */ |
| protected void updateDepthOffset() |
| { |
| depthOffset += getVerticalLegBuffer(); |
| } |
| |
| /** |
| * Updates the cellEditor based on editability of the JTree that we're |
| * contained in. If the tree is editable but doesn't have a cellEditor, a |
| * basic one will be used. |
| */ |
| protected void updateCellEditor() |
| { |
| if (tree.isEditable() && cellEditor == null) |
| setCellEditor(createDefaultCellEditor()); |
| createdCellEditor = true; |
| } |
| |
| /** |
| * Messaged from the tree we're in when the renderer has changed. |
| */ |
| protected void updateRenderer() |
| { |
| if (tree != null) |
| { |
| TreeCellRenderer rend = tree.getCellRenderer(); |
| if (rend != null) |
| { |
| createdRenderer = false; |
| currentCellRenderer = rend; |
| if (createdCellEditor) |
| tree.setCellEditor(null); |
| } |
| else |
| { |
| tree.setCellRenderer(createDefaultCellRenderer()); |
| createdRenderer = true; |
| } |
| } |
| else |
| { |
| currentCellRenderer = null; |
| createdRenderer = false; |
| } |
| |
| updateCellEditor(); |
| } |
| |
| /** |
| * Resets the treeState instance based on the tree we're providing the look |
| * and feel for. The node dimensions handler is required and must be created |
| * in advance. |
| */ |
| protected void configureLayoutCache() |
| { |
| treeState = createLayoutCache(); |
| treeState.setNodeDimensions(nodeDimensions); |
| } |
| |
| /** |
| * Marks the cached size as being invalid, and messages the tree with |
| * <code>treeDidChange</code>. |
| */ |
| protected void updateSize() |
| { |
| preferredSize = null; |
| updateCachedPreferredSize(); |
| tree.treeDidChange(); |
| } |
| |
| /** |
| * Updates the <code>preferredSize</code> instance variable, which is |
| * returned from <code>getPreferredSize()</code>. |
| */ |
| protected void updateCachedPreferredSize() |
| { |
| validCachedPreferredSize = false; |
| } |
| |
| /** |
| * Messaged from the VisibleTreeNode after it has been expanded. |
| * |
| * @param path is the path that has been expanded. |
| */ |
| protected void pathWasExpanded(TreePath path) |
| { |
| validCachedPreferredSize = false; |
| treeState.setExpandedState(path, true); |
| tree.repaint(); |
| } |
| |
| /** |
| * Messaged from the VisibleTreeNode after it has collapsed |
| */ |
| protected void pathWasCollapsed(TreePath path) |
| { |
| validCachedPreferredSize = false; |
| treeState.setExpandedState(path, false); |
| tree.repaint(); |
| } |
| |
| /** |
| * Install all defaults for the tree. |
| */ |
| protected void installDefaults() |
| { |
| LookAndFeel.installColorsAndFont(tree, "Tree.background", |
| "Tree.foreground", "Tree.font"); |
| |
| hashColor = UIManager.getColor("Tree.hash"); |
| if (hashColor == null) |
| hashColor = Color.black; |
| |
| tree.setOpaque(true); |
| |
| rightChildIndent = UIManager.getInt("Tree.rightChildIndent"); |
| leftChildIndent = UIManager.getInt("Tree.leftChildIndent"); |
| totalChildIndent = rightChildIndent + leftChildIndent; |
| setRowHeight(UIManager.getInt("Tree.rowHeight")); |
| tree.setRowHeight(getRowHeight()); |
| tree.setScrollsOnExpand(UIManager.getBoolean("Tree.scrollsOnExpand")); |
| setExpandedIcon(UIManager.getIcon("Tree.expandedIcon")); |
| setCollapsedIcon(UIManager.getIcon("Tree.collapsedIcon")); |
| } |
| |
| /** |
| * Install all keyboard actions for this |
| */ |
| protected void installKeyboardActions() |
| { |
| InputMap focusInputMap = |
| (InputMap) SharedUIDefaults.get("Tree.focusInputMap"); |
| SwingUtilities.replaceUIInputMap(tree, JComponent.WHEN_FOCUSED, |
| focusInputMap); |
| InputMap ancestorInputMap = |
| (InputMap) SharedUIDefaults.get("Tree.ancestorInputMap"); |
| SwingUtilities.replaceUIInputMap(tree, |
| JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, |
| ancestorInputMap); |
| |
| SwingUtilities.replaceUIActionMap(tree, getActionMap()); |
| } |
| |
| /** |
| * Creates and returns the shared action map for JTrees. |
| * |
| * @return the shared action map for JTrees |
| */ |
| private ActionMap getActionMap() |
| { |
| ActionMap am = (ActionMap) UIManager.get("Tree.actionMap"); |
| if (am == null) |
| { |
| am = createDefaultActions(); |
| UIManager.getLookAndFeelDefaults().put("Tree.actionMap", am); |
| } |
| return am; |
| } |
| |
| /** |
| * Creates the default actions when there are none specified by the L&F. |
| * |
| * @return the default actions |
| */ |
| private ActionMap createDefaultActions() |
| { |
| ActionMapUIResource am = new ActionMapUIResource(); |
| Action action; |
| |
| // TreeHomeAction. |
| action = new TreeHomeAction(-1, "selectFirst"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeHomeAction(-1, "selectFirstChangeLead"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeHomeAction(-1, "selectFirstExtendSelection"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeHomeAction(1, "selectLast"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeHomeAction(1, "selectLastChangeLead"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeHomeAction(1, "selectLastExtendSelection"); |
| am.put(action.getValue(Action.NAME), action); |
| |
| // TreeIncrementAction. |
| action = new TreeIncrementAction(-1, "selectPrevious"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeIncrementAction(-1, "selectPreviousExtendSelection"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeIncrementAction(-1, "selectPreviousChangeLead"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeIncrementAction(1, "selectNext"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeIncrementAction(1, "selectNextExtendSelection"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeIncrementAction(1, "selectNextChangeLead"); |
| am.put(action.getValue(Action.NAME), action); |
| |
| // TreeTraverseAction. |
| action = new TreeTraverseAction(-1, "selectParent"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeTraverseAction(1, "selectChild"); |
| am.put(action.getValue(Action.NAME), action); |
| |
| // TreeToggleAction. |
| action = new TreeToggleAction("toggleAndAnchor"); |
| am.put(action.getValue(Action.NAME), action); |
| |
| // TreePageAction. |
| action = new TreePageAction(-1, "scrollUpChangeSelection"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreePageAction(-1, "scrollUpExtendSelection"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreePageAction(-1, "scrollUpChangeLead"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreePageAction(1, "scrollDownChangeSelection"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreePageAction(1, "scrollDownExtendSelection"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreePageAction(1, "scrollDownChangeLead"); |
| am.put(action.getValue(Action.NAME), action); |
| |
| // Tree editing actions |
| action = new TreeStartEditingAction("startEditing"); |
| am.put(action.getValue(Action.NAME), action); |
| action = new TreeCancelEditingAction("cancel"); |
| am.put(action.getValue(Action.NAME), action); |
| |
| |
| return am; |
| } |
| |
| /** |
| * Converts the modifiers. |
| * |
| * @param mod - modifier to convert |
| * @returns the new modifier |
| */ |
| private int convertModifiers(int mod) |
| { |
| if ((mod & KeyEvent.SHIFT_DOWN_MASK) != 0) |
| { |
| mod |= KeyEvent.SHIFT_MASK; |
| mod &= ~ KeyEvent.SHIFT_DOWN_MASK; |
| } |
| if ((mod & KeyEvent.CTRL_DOWN_MASK) != 0) |
| { |
| mod |= KeyEvent.CTRL_MASK; |
| mod &= ~ KeyEvent.CTRL_DOWN_MASK; |
| } |
| if ((mod & KeyEvent.META_DOWN_MASK) != 0) |
| { |
| mod |= KeyEvent.META_MASK; |
| mod &= ~ KeyEvent.META_DOWN_MASK; |
| } |
| if ((mod & KeyEvent.ALT_DOWN_MASK) != 0) |
| { |
| mod |= KeyEvent.ALT_MASK; |
| mod &= ~ KeyEvent.ALT_DOWN_MASK; |
| } |
| if ((mod & KeyEvent.ALT_GRAPH_DOWN_MASK) != 0) |
| { |
| mod |= KeyEvent.ALT_GRAPH_MASK; |
| mod &= ~ KeyEvent.ALT_GRAPH_DOWN_MASK; |
| } |
| return mod; |
| } |
| |
| /** |
| * Install all listeners for this |
| */ |
| protected void installListeners() |
| { |
| propertyChangeListener = createPropertyChangeListener(); |
| tree.addPropertyChangeListener(propertyChangeListener); |
| |
| focusListener = createFocusListener(); |
| tree.addFocusListener(focusListener); |
| |
| treeSelectionListener = createTreeSelectionListener(); |
| tree.addTreeSelectionListener(treeSelectionListener); |
| |
| mouseListener = createMouseListener(); |
| tree.addMouseListener(mouseListener); |
| |
| keyListener = createKeyListener(); |
| tree.addKeyListener(keyListener); |
| |
| selectionModelPropertyChangeListener = |
| createSelectionModelPropertyChangeListener(); |
| if (treeSelectionModel != null |
| && selectionModelPropertyChangeListener != null) |
| { |
| treeSelectionModel.addPropertyChangeListener( |
| selectionModelPropertyChangeListener); |
| } |
| |
| componentListener = createComponentListener(); |
| tree.addComponentListener(componentListener); |
| |
| treeExpansionListener = createTreeExpansionListener(); |
| tree.addTreeExpansionListener(treeExpansionListener); |
| |
| treeModelListener = createTreeModelListener(); |
| if (treeModel != null) |
| treeModel.addTreeModelListener(treeModelListener); |
| |
| cellEditorListener = createCellEditorListener(); |
| } |
| |
| /** |
| * Install the UI for the component |
| * |
| * @param c the component to install UI for |
| */ |
| public void installUI(JComponent c) |
| { |
| tree = (JTree) c; |
| |
| prepareForUIInstall(); |
| installDefaults(); |
| installComponents(); |
| installKeyboardActions(); |
| installListeners(); |
| completeUIInstall(); |
| } |
| |
| /** |
| * Uninstall the defaults for the tree |
| */ |
| protected void uninstallDefaults() |
| { |
| tree.setFont(null); |
| tree.setForeground(null); |
| tree.setBackground(null); |
| } |
| |
| /** |
| * Uninstall the UI for the component |
| * |
| * @param c the component to uninstall UI for |
| */ |
| public void uninstallUI(JComponent c) |
| { |
| completeEditing(); |
| |
| prepareForUIUninstall(); |
| uninstallDefaults(); |
| uninstallKeyboardActions(); |
| uninstallListeners(); |
| uninstallComponents(); |
| completeUIUninstall(); |
| } |
| |
| /** |
| * Paints the specified component appropriate for the look and feel. This |
| * method is invoked from the ComponentUI.update method when the specified |
| * component is being painted. Subclasses should override this method and use |
| * the specified Graphics object to render the content of the component. |
| * |
| * @param g the Graphics context in which to paint |
| * @param c the component being painted; this argument is often ignored, but |
| * might be used if the UI object is stateless and shared by multiple |
| * components |
| */ |
| public void paint(Graphics g, JComponent c) |
| { |
| JTree tree = (JTree) c; |
| |
| int rows = treeState.getRowCount(); |
| |
| if (rows == 0) |
| // There is nothing to do if the tree is empty. |
| return; |
| |
| Rectangle clip = g.getClipBounds(); |
| |
| Insets insets = tree.getInsets(); |
| |
| if (clip != null && treeModel != null) |
| { |
| int startIndex = tree.getClosestRowForLocation(clip.x, clip.y); |
| int endIndex = tree.getClosestRowForLocation(clip.x + clip.width, |
| clip.y + clip.height); |
| |
| // Also paint dashes to the invisible nodes below. |
| // These should be painted first, otherwise they may cover |
| // the control icons. |
| if (endIndex < rows) |
| for (int i = endIndex + 1; i < rows; i++) |
| { |
| TreePath path = treeState.getPathForRow(i); |
| if (isLastChild(path)) |
| paintVerticalPartOfLeg(g, clip, insets, path); |
| } |
| |
| // The two loops are required to ensure that the lines are not |
| // painted over the other tree components. |
| |
| int n = endIndex - startIndex + 1; |
| Rectangle[] bounds = new Rectangle[n]; |
| boolean[] isLeaf = new boolean[n]; |
| boolean[] isExpanded = new boolean[n]; |
| TreePath[] path = new TreePath[n]; |
| int k; |
| |
| k = 0; |
| for (int i = startIndex; i <= endIndex; i++, k++) |
| { |
| path[k] = treeState.getPathForRow(i); |
| isLeaf[k] = treeModel.isLeaf(path[k].getLastPathComponent()); |
| isExpanded[k] = tree.isExpanded(path[k]); |
| bounds[k] = getPathBounds(tree, path[k]); |
| |
| paintHorizontalPartOfLeg(g, clip, insets, bounds[k], path[k], i, |
| isExpanded[k], false, isLeaf[k]); |
| if (isLastChild(path[k])) |
| paintVerticalPartOfLeg(g, clip, insets, path[k]); |
| } |
| |
| k = 0; |
| for (int i = startIndex; i <= endIndex; i++, k++) |
| { |
| paintRow(g, clip, insets, bounds[k], path[k], i, isExpanded[k], |
| false, isLeaf[k]); |
| } |
| } |
| } |
| |
| /** |
| * Check if the path is referring to the last child of some parent. |
| */ |
| private boolean isLastChild(TreePath path) |
| { |
| if (path instanceof GnuPath) |
| { |
| // Except the seldom case when the layout cache is changed, this |
| // optimized code will be executed. |
| return ((GnuPath) path).isLastChild; |
| } |
| else |
| { |
| // Non optimized general case. |
| TreePath parent = path.getParentPath(); |
| if (parent == null) |
| return false; |
| int childCount = treeState.getVisibleChildCount(parent); |
| int p = treeModel.getIndexOfChild(parent, path.getLastPathComponent()); |
| return p == childCount - 1; |
| } |
| } |
| |
| /** |
| * Ensures that the rows identified by beginRow through endRow are visible. |
| * |
| * @param beginRow is the first row |
| * @param endRow is the last row |
| */ |
| protected void ensureRowsAreVisible(int beginRow, int endRow) |
| { |
| if (beginRow < endRow) |
| { |
| int temp = endRow; |
| endRow = beginRow; |
| beginRow = temp; |
| } |
| |
| for (int i = beginRow; i < endRow; i++) |
| { |
| TreePath path = getPathForRow(tree, i); |
| if (! tree.isVisible(path)) |
| tree.makeVisible(path); |
| } |
| } |
| |
| /** |
| * Sets the preferred minimum size. |
| * |
| * @param newSize is the new preferred minimum size. |
| */ |
| public void setPreferredMinSize(Dimension newSize) |
| { |
| preferredMinSize = newSize; |
| } |
| |
| /** |
| * Gets the preferred minimum size. |
| * |
| * @returns the preferred minimum size. |
| */ |
| public Dimension getPreferredMinSize() |
| { |
| if (preferredMinSize == null) |
| return getPreferredSize(tree); |
| else |
| return preferredMinSize; |
| } |
| |
| /** |
| * Returns the preferred size to properly display the tree, this is a cover |
| * method for getPreferredSize(c, false). |
| * |
| * @param c the component whose preferred size is being queried; this argument |
| * is often ignored but might be used if the UI object is stateless |
| * and shared by multiple components |
| * @return the preferred size |
| */ |
| public Dimension getPreferredSize(JComponent c) |
| { |
| return getPreferredSize(c, false); |
| } |
| |
| /** |
| * Returns the preferred size to represent the tree in c. If checkConsistancy |
| * is true, checkConsistancy is messaged first. |
| * |
| * @param c the component whose preferred size is being queried. |
| * @param checkConsistancy if true must check consistancy |
| * @return the preferred size |
| */ |
| public Dimension getPreferredSize(JComponent c, boolean checkConsistancy) |
| { |
| if (! validCachedPreferredSize) |
| { |
| Rectangle size = tree.getBounds(); |
| // Add the scrollbar dimensions to the preferred size. |
| preferredSize = new Dimension(treeState.getPreferredWidth(size), |
| treeState.getPreferredHeight()); |
| validCachedPreferredSize = true; |
| } |
| return preferredSize; |
| } |
| |
| /** |
| * Returns the minimum size for this component. Which will be the min |
| * preferred size or (0,0). |
| * |
| * @param c the component whose min size is being queried. |
| * @returns the preferred size or null |
| */ |
| public Dimension getMinimumSize(JComponent c) |
| { |
| return preferredMinSize = getPreferredSize(c); |
| } |
| |
| /** |
| * Returns the maximum size for the component, which will be the preferred |
| * size if the instance is currently in JTree or (0,0). |
| * |
| * @param c the component whose preferred size is being queried |
| * @return the max size or null |
| */ |
| public Dimension getMaximumSize(JComponent c) |
| { |
| return getPreferredSize(c); |
| } |
| |
| /** |
| * Messages to stop the editing session. If the UI the receiver is providing |
| * the look and feel for returns true from |
| * <code>getInvokesStopCellEditing</code>, stopCellEditing will be invoked |
| * on the current editor. Then completeEditing will be messaged with false, |
| * true, false to cancel any lingering editing. |
| */ |
| protected void completeEditing() |
| { |
| completeEditing(false, true, false); |
| } |
| |
| /** |
| * Stops the editing session. If messageStop is true, the editor is messaged |
| * with stopEditing, if messageCancel is true the editor is messaged with |
| * cancelEditing. If messageTree is true, the treeModel is messaged with |
| * valueForPathChanged. |
| * |
| * @param messageStop message to stop editing |
| * @param messageCancel message to cancel editing |
| * @param messageTree message to treeModel |
| */ |
| protected void completeEditing(boolean messageStop, boolean messageCancel, |
| boolean messageTree) |
| { |
| // Make no attempt to complete the non existing editing session. |
| if (!isEditing(tree)) |
| return; |
| |
| if (messageStop) |
| { |
| getCellEditor().stopCellEditing(); |
| stopEditingInCompleteEditing = true; |
| } |
| |
| if (messageCancel) |
| { |
| getCellEditor().cancelCellEditing(); |
| stopEditingInCompleteEditing = true; |
| } |
| |
| if (messageTree) |
| { |
| TreeCellEditor editor = getCellEditor(); |
| if (editor != null) |
| { |
| Object value = editor.getCellEditorValue(); |
| treeModel.valueForPathChanged(tree.getLeadSelectionPath(), value); |
| } |
| } |
| } |
| |
| /** |
| * Will start editing for node if there is a cellEditor and shouldSelectCall |
| * returns true. This assumes that path is valid and visible. |
| * |
| * @param path is the path to start editing |
| * @param event is the MouseEvent performed on the path |
| * @return true if successful |
| */ |
| protected boolean startEditing(TreePath path, MouseEvent event) |
| { |
| updateCellEditor(); |
| TreeCellEditor ed = getCellEditor(); |
| |
| if (ed != null && (event == EDIT || ed.shouldSelectCell(event)) |
| && ed.isCellEditable(event)) |
| { |
| Rectangle bounds = getPathBounds(tree, path); |
| |
| // Extend the right boundary till the tree width. |
| bounds.width = tree.getWidth() - bounds.x; |
| |
| editingPath = path; |
| editingRow = tree.getRowForPath(editingPath); |
| |
| Object value = editingPath.getLastPathComponent(); |
| |
| stopEditingInCompleteEditing = false; |
| boolean expanded = tree.isExpanded(editingPath); |
| isEditing = true; |
| editingComponent = ed.getTreeCellEditorComponent(tree, value, true, |
| expanded, |
| isLeaf(editingRow), |
| editingRow); |
| |
| // Remove all previous components (if still present). Only one |
| // container with the editing component inside is allowed in the tree. |
| tree.removeAll(); |
| |
| // The editing component must be added to its container. We add the |
| // container, not the editing component itself. |
| Component container = editingComponent.getParent(); |
| container.setBounds(bounds); |
| tree.add(container); |
| editingComponent.requestFocus(); |
| |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * If the <code>mouseX</code> and <code>mouseY</code> are in the expand or |
| * collapse region of the row, this will toggle the row. |
| * |
| * @param path the path we are concerned with |
| * @param mouseX is the cursor's x position |
| * @param mouseY is the cursor's y position |
| */ |
| protected void checkForClickInExpandControl(TreePath path, int mouseX, |
| int mouseY) |
| { |
| if (isLocationInExpandControl(path, mouseX, mouseY)) |
| handleExpandControlClick(path, mouseX, mouseY); |
| } |
| |
| /** |
| * Returns true if the <code>mouseX</code> and <code>mouseY</code> fall in |
| * the area of row that is used to expand/collpse the node and the node at row |
| * does not represent a leaf. |
| * |
| * @param path the path we are concerned with |
| * @param mouseX is the cursor's x position |
| * @param mouseY is the cursor's y position |
| * @return true if the <code>mouseX</code> and <code>mouseY</code> fall in |
| * the area of row that is used to expand/collpse the node and the |
| * node at row does not represent a leaf. |
| */ |
| protected boolean isLocationInExpandControl(TreePath path, int mouseX, |
| int mouseY) |
| { |
| boolean cntlClick = false; |
| if (! treeModel.isLeaf(path.getLastPathComponent())) |
| { |
| int width; |
| Icon expandedIcon = getExpandedIcon(); |
| if (expandedIcon != null) |
| width = expandedIcon.getIconWidth(); |
| else |
| // Only guessing. This is the width of |
| // the tree control icon in Metal L&F. |
| width = 18; |
| |
| Insets i = tree.getInsets(); |
| |
| int depth; |
| if (isRootVisible()) |
| depth = path.getPathCount()-1; |
| else |
| depth = path.getPathCount()-2; |
| |
| int left = getRowX(tree.getRowForPath(path), depth) |
| - width + i.left; |
| cntlClick = mouseX >= left && mouseX <= left + width; |
| } |
| return cntlClick; |
| } |
| |
| /** |
| * Messaged when the user clicks the particular row, this invokes |
| * toggleExpandState. |
| * |
| * @param path the path we are concerned with |
| * @param mouseX is the cursor's x position |
| * @param mouseY is the cursor's y position |
| */ |
| protected void handleExpandControlClick(TreePath path, int mouseX, int mouseY) |
| { |
| toggleExpandState(path); |
| } |
| |
| /** |
| * Expands path if it is not expanded, or collapses row if it is expanded. If |
| * expanding a path and JTree scroll on expand, ensureRowsAreVisible is |
| * invoked to scroll as many of the children to visible as possible (tries to |
| * scroll to last visible descendant of path). |
| * |
| * @param path the path we are concerned with |
| */ |
| protected void toggleExpandState(TreePath path) |
| { |
| // tree.isExpanded(path) would do the same, but treeState knows faster. |
| if (treeState.isExpanded(path)) |
| tree.collapsePath(path); |
| else |
| tree.expandPath(path); |
| } |
| |
| /** |
| * Returning true signifies a mouse event on the node should toggle the |
| * selection of only the row under the mouse. The BasisTreeUI treats the |
| * event as "toggle selection event" if the CTRL button was pressed while |
| * clicking. The event is not counted as toggle event if the associated |
| * tree does not support the multiple selection. |
| * |
| * @param event is the MouseEvent performed on the row. |
| * @return true signifies a mouse event on the node should toggle the |
| * selection of only the row under the mouse. |
| */ |
| protected boolean isToggleSelectionEvent(MouseEvent event) |
| { |
| return |
| (tree.getSelectionModel().getSelectionMode() != |
| TreeSelectionModel.SINGLE_TREE_SELECTION) && |
| ((event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0); |
| } |
| |
| /** |
| * Returning true signifies a mouse event on the node should select from the |
| * anchor point. The BasisTreeUI treats the event as "multiple selection |
| * event" if the SHIFT button was pressed while clicking. The event is not |
| * counted as multiple selection event if the associated tree does not support |
| * the multiple selection. |
| * |
| * @param event is the MouseEvent performed on the node. |
| * @return true signifies a mouse event on the node should select from the |
| * anchor point. |
| */ |
| protected boolean isMultiSelectEvent(MouseEvent event) |
| { |
| return |
| (tree.getSelectionModel().getSelectionMode() != |
| TreeSelectionModel.SINGLE_TREE_SELECTION) && |
| ((event.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0); |
| } |
| |
| /** |
| * Returning true indicates the row under the mouse should be toggled based on |
| * the event. This is invoked after checkForClickInExpandControl, implying the |
| * location is not in the expand (toggle) control. |
| * |
| * @param event is the MouseEvent performed on the row. |
| * @return true indicates the row under the mouse should be toggled based on |
| * the event. |
| */ |
| protected boolean isToggleEvent(MouseEvent event) |
| { |
| boolean toggle = false; |
| if (SwingUtilities.isLeftMouseButton(event)) |
| { |
| int clickCount = tree.getToggleClickCount(); |
| if (clickCount > 0 && event.getClickCount() == clickCount) |
| toggle = true; |
| } |
| return toggle; |
| } |
| |
| /** |
| * Messaged to update the selection based on a MouseEvent over a particular |
| * row. If the even is a toggle selection event, the row is either selected, |
| * or deselected. If the event identifies a multi selection event, the |
| * selection is updated from the anchor point. Otherwise, the row is selected, |
| * and the previous selection is cleared.</p> |
| * |
| * @param path is the path selected for an event |
| * @param event is the MouseEvent performed on the path. |
| * |
| * @see #isToggleSelectionEvent(MouseEvent) |
| * @see #isMultiSelectEvent(MouseEvent) |
| */ |
| protected void selectPathForEvent(TreePath path, MouseEvent event) |
| { |
| if (isToggleSelectionEvent(event)) |
| { |
| // The event selects or unselects the clicked row. |
| if (tree.isPathSelected(path)) |
| tree.removeSelectionPath(path); |
| else |
| { |
| tree.addSelectionPath(path); |
| tree.setAnchorSelectionPath(path); |
| } |
| } |
| else if (isMultiSelectEvent(event)) |
| { |
| // The event extends selection form anchor till the clicked row. |
| TreePath anchor = tree.getAnchorSelectionPath(); |
| if (anchor != null) |
| { |
| int aRow = getRowForPath(tree, anchor); |
| tree.addSelectionInterval(aRow, getRowForPath(tree, path)); |
| } |
| else |
| tree.addSelectionPath(path); |
| } |
| else |
| { |
| // This is an ordinary event that just selects the clicked row. |
| tree.setSelectionPath(path); |
| if (isToggleEvent(event)) |
| toggleExpandState(path); |
| } |
| } |
| |
| /** |
| * Returns true if the node at <code>row</code> is a leaf. |
| * |
| * @param row is the row we are concerned with. |
| * @return true if the node at <code>row</code> is a leaf. |
| */ |
| protected boolean isLeaf(int row) |
| { |
| TreePath pathForRow = getPathForRow(tree, row); |
| if (pathForRow == null) |
| return true; |
| |
| Object node = pathForRow.getLastPathComponent(); |
| return treeModel.isLeaf(node); |
| } |
| |
| /** |
| * The action to start editing at the current lead selection path. |
| */ |
| class TreeStartEditingAction |
| extends AbstractAction |
| { |
| /** |
| * Creates the new tree cancel editing action. |
| * |
| * @param name the name of the action (used in toString). |
| */ |
| public TreeStartEditingAction(String name) |
| { |
| super(name); |
| } |
| |
| /** |
| * Start editing at the current lead selection path. |
| * |
| * @param e the ActionEvent that caused this action. |
| */ |
| public void actionPerformed(ActionEvent e) |
| { |
| TreePath lead = tree.getLeadSelectionPath(); |
| if (!tree.isEditing()) |
| tree.startEditingAtPath(lead); |
| } |
| } |
| |
| /** |
| * Updates the preferred size when scrolling, if necessary. |
| */ |
| public class ComponentHandler |
| extends ComponentAdapter |
| implements ActionListener |
| { |
| /** |
| * Timer used when inside a scrollpane and the scrollbar is adjusting |
| */ |
| protected Timer timer; |
| |
| /** ScrollBar that is being adjusted */ |
| protected JScrollBar scrollBar; |
| |
| /** |
| * Constructor |
| */ |
| public ComponentHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Invoked when the component's position changes. |
| * |
| * @param e the event that occurs when moving the component |
| */ |
| public void componentMoved(ComponentEvent e) |
| { |
| if (timer == null) |
| { |
| JScrollPane scrollPane = getScrollPane(); |
| if (scrollPane == null) |
| updateSize(); |
| else |
| { |
| // Determine the scrollbar that is adjusting, if any, and |
| // start the timer for that. If no scrollbar is adjusting, |
| // we simply call updateSize(). |
| scrollBar = scrollPane.getVerticalScrollBar(); |
| if (scrollBar == null || !scrollBar.getValueIsAdjusting()) |
| { |
| // It's not the vertical scrollbar, try the horizontal one. |
| scrollBar = scrollPane.getHorizontalScrollBar(); |
| if (scrollBar != null && scrollBar.getValueIsAdjusting()) |
| startTimer(); |
| else |
| updateSize(); |
| } |
| else |
| { |
| startTimer(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates, if necessary, and starts a Timer to check if needed to resize |
| * the bounds |
| */ |
| protected void startTimer() |
| { |
| if (timer == null) |
| { |
| timer = new Timer(200, this); |
| timer.setRepeats(true); |
| } |
| timer.start(); |
| } |
| |
| /** |
| * Returns the JScrollPane housing the JTree, or null if one isn't found. |
| * |
| * @return JScrollPane housing the JTree, or null if one isn't found. |
| */ |
| protected JScrollPane getScrollPane() |
| { |
| JScrollPane found = null; |
| Component p = tree.getParent(); |
| while (p != null && !(p instanceof JScrollPane)) |
| p = p.getParent(); |
| if (p instanceof JScrollPane) |
| found = (JScrollPane) p; |
| return found; |
| } |
| |
| /** |
| * Public as a result of Timer. If the scrollBar is null, or not adjusting, |
| * this stops the timer and updates the sizing. |
| * |
| * @param ae is the action performed |
| */ |
| public void actionPerformed(ActionEvent ae) |
| { |
| if (scrollBar == null || !scrollBar.getValueIsAdjusting()) |
| { |
| if (timer != null) |
| timer.stop(); |
| updateSize(); |
| timer = null; |
| scrollBar = null; |
| } |
| } |
| } |
| |
| /** |
| * Listener responsible for getting cell editing events and updating the tree |
| * accordingly. |
| */ |
| public class CellEditorHandler |
| implements CellEditorListener |
| { |
| /** |
| * Constructor |
| */ |
| public CellEditorHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Messaged when editing has stopped in the tree. Tells the listeners |
| * editing has stopped. |
| * |
| * @param e is the notification event |
| */ |
| public void editingStopped(ChangeEvent e) |
| { |
| stopEditing(tree); |
| } |
| |
| /** |
| * Messaged when editing has been canceled in the tree. This tells the |
| * listeners the editor has canceled editing. |
| * |
| * @param e is the notification event |
| */ |
| public void editingCanceled(ChangeEvent e) |
| { |
| cancelEditing(tree); |
| } |
| } // CellEditorHandler |
| |
| /** |
| * Repaints the lead selection row when focus is lost/grained. |
| */ |
| public class FocusHandler |
| implements FocusListener |
| { |
| /** |
| * Constructor |
| */ |
| public FocusHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Invoked when focus is activated on the tree we're in, redraws the lead |
| * row. Invoked when a component gains the keyboard focus. The method |
| * repaints the lead row that is shown differently when the tree is in |
| * focus. |
| * |
| * @param e is the focus event that is activated |
| */ |
| public void focusGained(FocusEvent e) |
| { |
| repaintLeadRow(); |
| } |
| |
| /** |
| * Invoked when focus is deactivated on the tree we're in, redraws the lead |
| * row. Invoked when a component loses the keyboard focus. The method |
| * repaints the lead row that is shown differently when the tree is in |
| * focus. |
| * |
| * @param e is the focus event that is deactivated |
| */ |
| public void focusLost(FocusEvent e) |
| { |
| repaintLeadRow(); |
| } |
| |
| /** |
| * Repaint the lead row. |
| */ |
| void repaintLeadRow() |
| { |
| TreePath lead = tree.getLeadSelectionPath(); |
| if (lead != null) |
| tree.repaint(tree.getPathBounds(lead)); |
| } |
| } |
| |
| /** |
| * This is used to get multiple key down events to appropriately genereate |
| * events. |
| */ |
| public class KeyHandler |
| extends KeyAdapter |
| { |
| /** Key code that is being generated for. */ |
| protected Action repeatKeyAction; |
| |
| /** Set to true while keyPressed is active */ |
| protected boolean isKeyDown; |
| |
| /** |
| * Constructor |
| */ |
| public KeyHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Invoked when a key has been typed. Moves the keyboard focus to the first |
| * element whose first letter matches the alphanumeric key pressed by the |
| * user. Subsequent same key presses move the keyboard focus to the next |
| * object that starts with the same letter. |
| * |
| * @param e the key typed |
| */ |
| public void keyTyped(KeyEvent e) |
| { |
| char typed = Character.toLowerCase(e.getKeyChar()); |
| for (int row = tree.getLeadSelectionRow() + 1; |
| row < tree.getRowCount(); row++) |
| { |
| if (checkMatch(row, typed)) |
| { |
| tree.setSelectionRow(row); |
| tree.scrollRowToVisible(row); |
| return; |
| } |
| } |
| |
| // Not found below, search above: |
| for (int row = 0; row < tree.getLeadSelectionRow(); row++) |
| { |
| if (checkMatch(row, typed)) |
| { |
| tree.setSelectionRow(row); |
| tree.scrollRowToVisible(row); |
| return; |
| } |
| } |
| } |
| |
| /** |
| * Check if the given tree row starts with this character |
| * |
| * @param row the tree row |
| * @param typed the typed char, must be converted to lowercase |
| * @return true if the given tree row starts with this character |
| */ |
| boolean checkMatch(int row, char typed) |
| { |
| TreePath path = treeState.getPathForRow(row); |
| String node = path.getLastPathComponent().toString(); |
| if (node.length() > 0) |
| { |
| char x = node.charAt(0); |
| if (typed == Character.toLowerCase(x)) |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Invoked when a key has been pressed. |
| * |
| * @param e the key pressed |
| */ |
| public void keyPressed(KeyEvent e) |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Invoked when a key has been released |
| * |
| * @param e the key released |
| */ |
| public void keyReleased(KeyEvent e) |
| { |
| // Nothing to do here. |
| } |
| } |
| |
| /** |
| * MouseListener is responsible for updating the selection based on mouse |
| * events. |
| */ |
| public class MouseHandler |
| extends MouseAdapter |
| implements MouseMotionListener |
| { |
| /** |
| * Constructor |
| */ |
| public MouseHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Invoked when a mouse button has been pressed on a component. |
| * |
| * @param e is the mouse event that occured |
| */ |
| public void mousePressed(MouseEvent e) |
| { |
| // Any mouse click cancels the previous waiting edit action, initiated |
| // by the single click on the selected node. |
| if (startEditTimer != null) |
| { |
| startEditTimer.stop(); |
| startEditTimer = null; |
| } |
| |
| if (tree != null && tree.isEnabled()) |
| { |
| // Always end the current editing session if clicked on the |
| // tree and outside the bounds of the editing component. |
| if (isEditing(tree)) |
| if (!stopEditing(tree)) |
| // Return if we have failed to cancel the editing session. |
| return; |
| |
| int x = e.getX(); |
| int y = e.getY(); |
| TreePath path = getClosestPathForLocation(tree, x, y); |
| |
| if (path != null) |
| { |
| Rectangle bounds = getPathBounds(tree, path); |
| if (SwingUtilities.isLeftMouseButton(e)) |
| checkForClickInExpandControl(path, x, y); |
| |
| if (x > bounds.x && x <= (bounds.x + bounds.width)) |
| { |
| TreePath currentLead = tree.getLeadSelectionPath(); |
| if (currentLead != null && currentLead.equals(path) |
| && e.getClickCount() == 1 && tree.isEditable()) |
| { |
| // Schedule the editing session. |
| final TreePath editPath = path; |
| |
| // The code below handles the required click-pause-click |
| // functionality which must be present in the tree UI. |
| // If the next click comes after the |
| // time longer than the double click interval AND |
| // the same node stays focused for the WAIT_TILL_EDITING |
| // duration, the timer starts the editing session. |
| if (startEditTimer != null) |
| startEditTimer.stop(); |
| |
| startEditTimer = new Timer(WAIT_TILL_EDITING, |
| new ActionListener() |
| { |
| public void actionPerformed(ActionEvent e) |
| { |
| startEditing(editPath, EDIT); |
| } |
| }); |
| |
| startEditTimer.setRepeats(false); |
| startEditTimer.start(); |
| } |
| else |
| { |
| if (e.getClickCount() == 2) |
| toggleExpandState(path); |
| else |
| selectPathForEvent(path, e); |
| } |
| } |
| } |
| } |
| |
| // We need to request the focus. |
| tree.requestFocusInWindow(); |
| } |
| |
| /** |
| * Invoked when a mouse button is pressed on a component and then dragged. |
| * MOUSE_DRAGGED events will continue to be delivered to the component where |
| * the drag originated until the mouse button is released (regardless of |
| * whether the mouse position is within the bounds of the component). |
| * |
| * @param e is the mouse event that occured |
| */ |
| public void mouseDragged(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Invoked when the mouse button has been moved on a component (with no |
| * buttons no down). |
| * |
| * @param e the mouse event that occured |
| */ |
| public void mouseMoved(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Invoked when a mouse button has been released on a component. |
| * |
| * @param e is the mouse event that occured |
| */ |
| public void mouseReleased(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| } |
| |
| /** |
| * MouseInputHandler handles passing all mouse events, including mouse motion |
| * events, until the mouse is released to the destination it is constructed |
| * with. |
| */ |
| public class MouseInputHandler |
| implements MouseInputListener |
| { |
| /** Source that events are coming from */ |
| protected Component source; |
| |
| /** Destination that receives all events. */ |
| protected Component destination; |
| |
| /** |
| * Constructor |
| * |
| * @param source that events are coming from |
| * @param destination that receives all events |
| * @param e is the event received |
| */ |
| public MouseInputHandler(Component source, Component destination, |
| MouseEvent e) |
| { |
| this.source = source; |
| this.destination = destination; |
| } |
| |
| /** |
| * Invoked when the mouse button has been clicked (pressed and released) on |
| * a component. |
| * |
| * @param e mouse event that occured |
| */ |
| public void mouseClicked(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Invoked when a mouse button has been pressed on a component. |
| * |
| * @param e mouse event that occured |
| */ |
| public void mousePressed(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Invoked when a mouse button has been released on a component. |
| * |
| * @param e mouse event that occured |
| */ |
| public void mouseReleased(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Invoked when the mouse enters a component. |
| * |
| * @param e mouse event that occured |
| */ |
| public void mouseEntered(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Invoked when the mouse exits a component. |
| * |
| * @param e mouse event that occured |
| */ |
| public void mouseExited(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Invoked when a mouse button is pressed on a component and then dragged. |
| * MOUSE_DRAGGED events will continue to be delivered to the component where |
| * the drag originated until the mouse button is released (regardless of |
| * whether the mouse position is within the bounds of the component). |
| * |
| * @param e mouse event that occured |
| */ |
| public void mouseDragged(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Invoked when the mouse cursor has been moved onto a component but no |
| * buttons have been pushed. |
| * |
| * @param e mouse event that occured |
| */ |
| public void mouseMoved(MouseEvent e) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| |
| /** |
| * Removes event from the source |
| */ |
| protected void removeFromSource() |
| throws NotImplementedException |
| { |
| // TODO: Implement this properly. |
| } |
| } |
| |
| /** |
| * Class responsible for getting size of node, method is forwarded to |
| * BasicTreeUI method. X location does not include insets, that is handled in |
| * getPathBounds. |
| */ |
| public class NodeDimensionsHandler |
| extends AbstractLayoutCache.NodeDimensions |
| { |
| /** |
| * Constructor |
| */ |
| public NodeDimensionsHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Returns, by reference in bounds, the size and x origin to place value at. |
| * The calling method is responsible for determining the Y location. If |
| * bounds is null, a newly created Rectangle should be returned, otherwise |
| * the value should be placed in bounds and returned. |
| * |
| * @param cell the value to be represented |
| * @param row row being queried |
| * @param depth the depth of the row |
| * @param expanded true if row is expanded |
| * @param size a Rectangle containing the size needed to represent value |
| * @return containing the node dimensions, or null if node has no dimension |
| */ |
| public Rectangle getNodeDimensions(Object cell, int row, int depth, |
| boolean expanded, Rectangle size) |
| { |
| if (size == null || cell == null) |
| return null; |
| |
| String s = cell.toString(); |
| Font f = tree.getFont(); |
| FontMetrics fm = tree.getToolkit().getFontMetrics(f); |
| |
| if (s != null) |
| { |
| TreePath path = treeState.getPathForRow(row); |
| size.x = getRowX(row, depth); |
| size.width = SwingUtilities.computeStringWidth(fm, s); |
| size.width = size.width + getCurrentControlIcon(path).getIconWidth() |
| + gap + getNodeIcon(path).getIconWidth(); |
| size.height = getMaxHeight(tree); |
| size.y = size.height * row; |
| } |
| |
| return size; |
| } |
| |
| /** |
| * Returns the amount to indent the given row |
| * |
| * @return amount to indent the given row. |
| */ |
| protected int getRowX(int row, int depth) |
| { |
| return BasicTreeUI.this.getRowX(row, depth); |
| } |
| } // NodeDimensionsHandler |
| |
| /** |
| * PropertyChangeListener for the tree. Updates the appropriate variable, or |
| * TreeState, based on what changes. |
| */ |
| public class PropertyChangeHandler |
| implements PropertyChangeListener |
| { |
| |
| /** |
| * Constructor |
| */ |
| public PropertyChangeHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * This method gets called when a bound property is changed. |
| * |
| * @param event A PropertyChangeEvent object describing the event source and |
| * the property that has changed. |
| */ |
| public void propertyChange(PropertyChangeEvent event) |
| { |
| String property = event.getPropertyName(); |
| if (property.equals(JTree.ROOT_VISIBLE_PROPERTY)) |
| { |
| validCachedPreferredSize = false; |
| treeState.setRootVisible(tree.isRootVisible()); |
| tree.repaint(); |
| } |
| else if (property.equals(JTree.SELECTION_MODEL_PROPERTY)) |
| { |
| treeSelectionModel = tree.getSelectionModel(); |
| treeSelectionModel.setRowMapper(treeState); |
| } |
| else if (property.equals(JTree.TREE_MODEL_PROPERTY)) |
| { |
| setModel(tree.getModel()); |
| } |
| else if (property.equals(JTree.CELL_RENDERER_PROPERTY)) |
| { |
| setCellRenderer(tree.getCellRenderer()); |
| // Update layout. |
| if (treeState != null) |
| treeState.invalidateSizes(); |
| } |
| } |
| } |
| |
| /** |
| * Listener on the TreeSelectionModel, resets the row selection if any of the |
| * properties of the model change. |
| */ |
| public class SelectionModelPropertyChangeHandler |
| implements PropertyChangeListener |
| { |
| |
| /** |
| * Constructor |
| */ |
| public SelectionModelPropertyChangeHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * This method gets called when a bound property is changed. |
| * |
| * @param event A PropertyChangeEvent object describing the event source and |
| * the property that has changed. |
| */ |
| public void propertyChange(PropertyChangeEvent event) |
| throws NotImplementedException |
| { |
| // TODO: What should be done here, if anything? |
| } |
| } |
| |
| /** |
| * The action to cancel editing on this tree. |
| */ |
| public class TreeCancelEditingAction |
| extends AbstractAction |
| { |
| /** |
| * Creates the new tree cancel editing action. |
| * |
| * @param name the name of the action (used in toString). |
| */ |
| public TreeCancelEditingAction(String name) |
| { |
| super(name); |
| } |
| |
| /** |
| * Invoked when an action occurs, cancels the cell editing (if the |
| * tree cell is being edited). |
| * |
| * @param e event that occured |
| */ |
| public void actionPerformed(ActionEvent e) |
| { |
| if (isEnabled() && tree.isEditing()) |
| tree.cancelEditing(); |
| } |
| } |
| |
| /** |
| * Updates the TreeState in response to nodes expanding/collapsing. |
| */ |
| public class TreeExpansionHandler |
| implements TreeExpansionListener |
| { |
| |
| /** |
| * Constructor |
| */ |
| public TreeExpansionHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Called whenever an item in the tree has been expanded. |
| * |
| * @param event is the event that occured |
| */ |
| public void treeExpanded(TreeExpansionEvent event) |
| { |
| validCachedPreferredSize = false; |
| treeState.setExpandedState(event.getPath(), true); |
| // The maximal cell height may change |
| maxHeight = 0; |
| tree.revalidate(); |
| tree.repaint(); |
| } |
| |
| /** |
| * Called whenever an item in the tree has been collapsed. |
| * |
| * @param event is the event that occured |
| */ |
| public void treeCollapsed(TreeExpansionEvent event) |
| { |
| validCachedPreferredSize = false; |
| treeState.setExpandedState(event.getPath(), false); |
| // The maximal cell height may change |
| maxHeight = 0; |
| tree.revalidate(); |
| tree.repaint(); |
| } |
| } // TreeExpansionHandler |
| |
| /** |
| * TreeHomeAction is used to handle end/home actions. Scrolls either the first |
| * or last cell to be visible based on direction. |
| */ |
| public class TreeHomeAction |
| extends AbstractAction |
| { |
| |
| /** The direction, either home or end */ |
| protected int direction; |
| |
| /** |
| * Creates a new TreeHomeAction instance. |
| * |
| * @param dir the direction to go to, <code>-1</code> for home, |
| * <code>1</code> for end |
| * @param name the name of the action |
| */ |
| public TreeHomeAction(int dir, String name) |
| { |
| direction = dir; |
| putValue(Action.NAME, name); |
| } |
| |
| /** |
| * Invoked when an action occurs. |
| * |
| * @param e is the event that occured |
| */ |
| public void actionPerformed(ActionEvent e) |
| { |
| if (tree != null) |
| { |
| String command = (String) getValue(Action.NAME); |
| if (command.equals("selectFirst")) |
| { |
| ensureRowsAreVisible(0, 0); |
| tree.setSelectionInterval(0, 0); |
| } |
| if (command.equals("selectFirstChangeLead")) |
| { |
| ensureRowsAreVisible(0, 0); |
| tree.setLeadSelectionPath(getPathForRow(tree, 0)); |
| } |
| if (command.equals("selectFirstExtendSelection")) |
| { |
| ensureRowsAreVisible(0, 0); |
| TreePath anchorPath = tree.getAnchorSelectionPath(); |
| if (anchorPath == null) |
| tree.setSelectionInterval(0, 0); |
| else |
| { |
| int anchorRow = getRowForPath(tree, anchorPath); |
| tree.setSelectionInterval(0, anchorRow); |
| tree.setAnchorSelectionPath(anchorPath); |
| tree.setLeadSelectionPath(getPathForRow(tree, 0)); |
| } |
| } |
| else if (command.equals("selectLast")) |
| { |
| int end = getRowCount(tree) - 1; |
| ensureRowsAreVisible(end, end); |
| tree.setSelectionInterval(end, end); |
| } |
| else if (command.equals("selectLastChangeLead")) |
| { |
| int end = getRowCount(tree) - 1; |
| ensureRowsAreVisible(end, end); |
| tree.setLeadSelectionPath(getPathForRow(tree, end)); |
| } |
| else if (command.equals("selectLastExtendSelection")) |
| { |
| int end = getRowCount(tree) - 1; |
| ensureRowsAreVisible(end, end); |
| TreePath anchorPath = tree.getAnchorSelectionPath(); |
| if (anchorPath == null) |
| tree.setSelectionInterval(end, end); |
| else |
| { |
| int anchorRow = getRowForPath(tree, anchorPath); |
| tree.setSelectionInterval(end, anchorRow); |
| tree.setAnchorSelectionPath(anchorPath); |
| tree.setLeadSelectionPath(getPathForRow(tree, end)); |
| } |
| } |
| } |
| |
| // Ensure that the lead path is visible after the increment action. |
| tree.scrollPathToVisible(tree.getLeadSelectionPath()); |
| } |
| |
| /** |
| * Returns true if the action is enabled. |
| * |
| * @return true if the action is enabled. |
| */ |
| public boolean isEnabled() |
| { |
| return (tree != null) && tree.isEnabled(); |
| } |
| } |
| |
| /** |
| * TreeIncrementAction is used to handle up/down actions. Selection is moved |
| * up or down based on direction. |
| */ |
| public class TreeIncrementAction |
| extends AbstractAction |
| { |
| |
| /** |
| * Specifies the direction to adjust the selection by. |
| */ |
| protected int direction; |
| |
| /** |
| * Creates a new TreeIncrementAction. |
| * |
| * @param dir up or down, <code>-1</code> for up, <code>1</code> for down |
| * @param name is the name of the direction |
| */ |
| public TreeIncrementAction(int dir, String name) |
| { |
| direction = dir; |
| putValue(Action.NAME, name); |
| } |
| |
| /** |
| * Invoked when an action occurs. |
| * |
| * @param e is the event that occured |
| */ |
| public void actionPerformed(ActionEvent e) |
| { |
| TreePath currentPath = tree.getLeadSelectionPath(); |
| int currentRow; |
| |
| if (currentPath != null) |
| currentRow = treeState.getRowForPath(currentPath); |
| else |
| currentRow = 0; |
| |
| int rows = treeState.getRowCount(); |
| |
| int nextRow = currentRow + 1; |
| int prevRow = currentRow - 1; |
| boolean hasNext = nextRow < rows; |
| boolean hasPrev = prevRow >= 0 && rows > 0; |
| TreePath newPath; |
| String command = (String) getValue(Action.NAME); |
| |
| if (command.equals("selectPreviousChangeLead") && hasPrev) |
| { |
| newPath = treeState.getPathForRow(prevRow); |
| tree.setSelectionPath(newPath); |
| tree.setAnchorSelectionPath(newPath); |
| tree.setLeadSelectionPath(newPath); |
| } |
| else if (command.equals("selectPreviousExtendSelection") && hasPrev) |
| { |
| newPath = treeState.getPathForRow(prevRow); |
| |
| // If the new path is already selected, the selection shrinks, |
| // unselecting the previously current path. |
| if (tree.isPathSelected(newPath)) |
| tree.getSelectionModel().removeSelectionPath(currentPath); |
| |
| // This must be called in any case because it updates the model |
| // lead selection index. |
| tree.addSelectionPath(newPath); |
| tree.setLeadSelectionPath(newPath); |
| } |
| else if (command.equals("selectPrevious") && hasPrev) |
| { |
| newPath = treeState.getPathForRow(prevRow); |
| tree.setSelectionPath(newPath); |
| } |
| else if (command.equals("selectNext") && hasNext) |
| { |
| newPath = treeState.getPathForRow(nextRow); |
| tree.setSelectionPath(newPath); |
| } |
| else if (command.equals("selectNextExtendSelection") && hasNext) |
| { |
| newPath = treeState.getPathForRow(nextRow); |
| |
| // If the new path is already selected, the selection shrinks, |
| // unselecting the previously current path. |
| if (tree.isPathSelected(newPath)) |
| tree.getSelectionModel().removeSelectionPath(currentPath); |
| |
| // This must be called in any case because it updates the model |
| // lead selection index. |
| tree.addSelectionPath(newPath); |
| |
| tree.setLeadSelectionPath(newPath); |
| } |
| else if (command.equals("selectNextChangeLead") && hasNext) |
| { |
| newPath = treeState.getPathForRow(nextRow); |
| tree.setSelectionPath(newPath); |
| tree.setAnchorSelectionPath(newPath); |
| tree.setLeadSelectionPath(newPath); |
| } |
| |
| // Ensure that the lead path is visible after the increment action. |
| tree.scrollPathToVisible(tree.getLeadSelectionPath()); |
| } |
| |
| /** |
| * Returns true if the action is enabled. |
| * |
| * @return true if the action is enabled. |
| */ |
| public boolean isEnabled() |
| { |
| return (tree != null) && tree.isEnabled(); |
| } |
| } |
| |
| /** |
| * Forwards all TreeModel events to the TreeState. |
| */ |
| public class TreeModelHandler |
| implements TreeModelListener |
| { |
| /** |
| * Constructor |
| */ |
| public TreeModelHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Invoked after a node (or a set of siblings) has changed in some way. The |
| * node(s) have not changed locations in the tree or altered their children |
| * arrays, but other attributes have changed and may affect presentation. |
| * Example: the name of a file has changed, but it is in the same location |
| * in the file system. To indicate the root has changed, childIndices and |
| * children will be null. Use e.getPath() to get the parent of the changed |
| * node(s). e.getChildIndices() returns the index(es) of the changed |
| * node(s). |
| * |
| * @param e is the event that occured |
| */ |
| public void treeNodesChanged(TreeModelEvent e) |
| { |
| validCachedPreferredSize = false; |
| treeState.treeNodesChanged(e); |
| tree.repaint(); |
| } |
| |
| /** |
| * Invoked after nodes have been inserted into the tree. Use e.getPath() to |
| * get the parent of the new node(s). e.getChildIndices() returns the |
| * index(es) of the new node(s) in ascending order. |
| * |
| * @param e is the event that occured |
| */ |
| public void treeNodesInserted(TreeModelEvent e) |
| { |
| validCachedPreferredSize = false; |
| treeState.treeNodesInserted(e); |
| tree.repaint(); |
| } |
| |
| /** |
| * Invoked after nodes have been removed from the tree. Note that if a |
| * subtree is removed from the tree, this method may only be invoked once |
| * for the root of the removed subtree, not once for each individual set of |
| * siblings removed. Use e.getPath() to get the former parent of the deleted |
| * node(s). e.getChildIndices() returns, in ascending order, the index(es) |
| * the node(s) had before being deleted. |
| * |
| * @param e is the event that occured |
| */ |
| public void treeNodesRemoved(TreeModelEvent e) |
| { |
| validCachedPreferredSize = false; |
| treeState.treeNodesRemoved(e); |
| tree.repaint(); |
| } |
| |
| /** |
| * Invoked after the tree has drastically changed structure from a given |
| * node down. If the path returned by e.getPath() is of length one and the |
| * first element does not identify the current root node the first element |
| * should become the new root of the tree. Use e.getPath() to get the path |
| * to the node. e.getChildIndices() returns null. |
| * |
| * @param e is the event that occured |
| */ |
| public void treeStructureChanged(TreeModelEvent e) |
| { |
| if (e.getPath().length == 1 |
| && ! e.getPath()[0].equals(treeModel.getRoot())) |
| tree.expandPath(new TreePath(treeModel.getRoot())); |
| validCachedPreferredSize = false; |
| treeState.treeStructureChanged(e); |
| tree.repaint(); |
| } |
| } // TreeModelHandler |
| |
| /** |
| * TreePageAction handles page up and page down events. |
| */ |
| public class TreePageAction |
| extends AbstractAction |
| { |
| /** Specifies the direction to adjust the selection by. */ |
| protected int direction; |
| |
| /** |
| * Constructor |
| * |
| * @param direction up or down |
| * @param name is the name of the direction |
| */ |
| public TreePageAction(int direction, String name) |
| { |
| this.direction = direction; |
| putValue(Action.NAME, name); |
| } |
| |
| /** |
| * Invoked when an action occurs. |
| * |
| * @param e is the event that occured |
| */ |
| public void actionPerformed(ActionEvent e) |
| { |
| String command = (String) getValue(Action.NAME); |
| boolean extendSelection = command.equals("scrollUpExtendSelection") |
| || command.equals("scrollDownExtendSelection"); |
| boolean changeSelection = command.equals("scrollUpChangeSelection") |
| || command.equals("scrollDownChangeSelection"); |
| |
| // Disable change lead, unless we are in discontinuous mode. |
| if (!extendSelection && !changeSelection |
| && tree.getSelectionModel().getSelectionMode() != |
| TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION) |
| { |
| changeSelection = true; |
| } |
| |
| int rowCount = getRowCount(tree); |
| if (rowCount > 0 && treeSelectionModel != null) |
| { |
| Dimension maxSize = tree.getSize(); |
| TreePath lead = tree.getLeadSelectionPath(); |
| TreePath newPath = null; |
| Rectangle visible = tree.getVisibleRect(); |
| if (direction == -1) // The RI handles -1 as up. |
| { |
| newPath = getClosestPathForLocation(tree, visible.x, visible.y); |
| if (newPath.equals(lead)) // Corner case, adjust one page up. |
| { |
| visible.y = Math.max(0, visible.y - visible.height); |
| newPath = getClosestPathForLocation(tree, visible.x, |
| visible.y); |
| } |
| } |
| else // +1 is down. |
| { |
| visible.y = Math.min(maxSize.height, |
| visible.y + visible.height - 1); |
| newPath = getClosestPathForLocation(tree, visible.x, visible.y); |
| if (newPath.equals(lead)) // Corner case, adjust one page down. |
| { |
| visible.y = Math.min(maxSize.height, |
| visible.y + visible.height - 1); |
| newPath = getClosestPathForLocation(tree, visible.x, |
| visible.y); |
| } |
| } |
| |
| // Determine new visible rect. |
| Rectangle newVisible = getPathBounds(tree, newPath); |
| newVisible.x = visible.x; |
| newVisible.width = visible.width; |
| if (direction == -1) |
| { |
| newVisible.height = visible.height; |
| } |
| else |
| { |
| newVisible.y -= visible.height - newVisible.height; |
| newVisible.height = visible.height; |
| } |
| |
| if (extendSelection) |
| { |
| // Extend selection. |
| TreePath anchorPath = tree.getAnchorSelectionPath(); |
| if (anchorPath == null) |
| { |
| tree.setSelectionPath(newPath); |
| } |
| else |
| { |
| int newIndex = getRowForPath(tree, newPath); |
| int anchorIndex = getRowForPath(tree, anchorPath); |
| tree.setSelectionInterval(Math.min(anchorIndex, newIndex), |
| Math.max(anchorIndex, newIndex)); |
| tree.setAnchorSelectionPath(anchorPath); |
| tree.setLeadSelectionPath(newPath); |
| } |
| } |
| else if (changeSelection) |
| { |
| tree.setSelectionPath(newPath); |
| } |
| else // Change lead. |
| { |
| tree.setLeadSelectionPath(newPath); |
| } |
| |
| tree.scrollRectToVisible(newVisible); |
| } |
| } |
| |
| /** |
| * Returns true if the action is enabled. |
| * |
| * @return true if the action is enabled. |
| */ |
| public boolean isEnabled() |
| { |
| return (tree != null) && tree.isEnabled(); |
| } |
| } // TreePageAction |
| |
| /** |
| * Listens for changes in the selection model and updates the display |
| * accordingly. |
| */ |
| public class TreeSelectionHandler |
| implements TreeSelectionListener |
| { |
| /** |
| * Constructor |
| */ |
| public TreeSelectionHandler() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Messaged when the selection changes in the tree we're displaying for. |
| * Stops editing, messages super and displays the changed paths. |
| * |
| * @param event the event that characterizes the change. |
| */ |
| public void valueChanged(TreeSelectionEvent event) |
| { |
| if (tree.isEditing()) |
| tree.cancelEditing(); |
| |
| TreePath op = event.getOldLeadSelectionPath(); |
| TreePath np = event.getNewLeadSelectionPath(); |
| |
| // Repaint of the changed lead selection path. |
| if (op != np) |
| { |
| Rectangle o = treeState.getBounds(event.getOldLeadSelectionPath(), |
| new Rectangle()); |
| Rectangle n = treeState.getBounds(event.getNewLeadSelectionPath(), |
| new Rectangle()); |
| |
| if (o != null) |
| tree.repaint(o); |
| if (n != null) |
| tree.repaint(n); |
| } |
| } |
| } // TreeSelectionHandler |
| |
| /** |
| * For the first selected row expandedness will be toggled. |
| */ |
| public class TreeToggleAction |
| extends AbstractAction |
| { |
| /** |
| * Creates a new TreeToggleAction. |
| * |
| * @param name is the name of <code>Action</code> field |
| */ |
| public TreeToggleAction(String name) |
| { |
| putValue(Action.NAME, name); |
| } |
| |
| /** |
| * Invoked when an action occurs. |
| * |
| * @param e the event that occured |
| */ |
| public void actionPerformed(ActionEvent e) |
| { |
| int selected = tree.getLeadSelectionRow(); |
| if (selected != -1 && isLeaf(selected)) |
| { |
| TreePath anchorPath = tree.getAnchorSelectionPath(); |
| TreePath leadPath = tree.getLeadSelectionPath(); |
| toggleExpandState(getPathForRow(tree, selected)); |
| // Need to do this, so that the toggling doesn't mess up the lead |
| // and anchor. |
| tree.setLeadSelectionPath(leadPath); |
| tree.setAnchorSelectionPath(anchorPath); |
| |
| // Ensure that the lead path is visible after the increment action. |
| tree.scrollPathToVisible(tree.getLeadSelectionPath()); |
| } |
| } |
| |
| /** |
| * Returns true if the action is enabled. |
| * |
| * @return true if the action is enabled, false otherwise |
| */ |
| public boolean isEnabled() |
| { |
| return (tree != null) && tree.isEnabled(); |
| } |
| } // TreeToggleAction |
| |
| /** |
| * TreeTraverseAction is the action used for left/right keys. Will toggle the |
| * expandedness of a node, as well as potentially incrementing the selection. |
| */ |
| public class TreeTraverseAction |
| extends AbstractAction |
| { |
| /** |
| * Determines direction to traverse, 1 means expand, -1 means collapse. |
| */ |
| protected int direction; |
| |
| /** |
| * Constructor |
| * |
| * @param direction to traverse |
| * @param name is the name of the direction |
| */ |
| public TreeTraverseAction(int direction, String name) |
| { |
| this.direction = direction; |
| putValue(Action.NAME, name); |
| } |
| |
| /** |
| * Invoked when an action occurs. |
| * |
| * @param e the event that occured |
| */ |
| public void actionPerformed(ActionEvent e) |
| { |
| TreePath current = tree.getLeadSelectionPath(); |
| if (current == null) |
| return; |
| |
| String command = (String) getValue(Action.NAME); |
| if (command.equals("selectParent")) |
| { |
| if (current == null) |
| return; |
| |
| if (tree.isExpanded(current)) |
| { |
| tree.collapsePath(current); |
| } |
| else |
| { |
| // If the node is not expanded (also, if it is a leaf node), |
| // we just select the parent. We do not select the root if it |
| // is not visible. |
| TreePath parent = current.getParentPath(); |
| if (parent != null && |
| ! (parent.getPathCount() == 1 && ! tree.isRootVisible())) |
| tree.setSelectionPath(parent); |
| } |
| } |
| else if (command.equals("selectChild")) |
| { |
| Object node = current.getLastPathComponent(); |
| int nc = treeModel.getChildCount(node); |
| if (nc == 0 || treeState.isExpanded(current)) |
| { |
| // If the node is leaf or it is already expanded, |
| // we just select the next row. |
| int nextRow = tree.getLeadSelectionRow() + 1; |
| if (nextRow <= tree.getRowCount()) |
| tree.setSelectionRow(nextRow); |
| } |
| else |
| { |
| tree.expandPath(current); |
| } |
| } |
| |
| // Ensure that the lead path is visible after the increment action. |
| tree.scrollPathToVisible(tree.getLeadSelectionPath()); |
| } |
| |
| /** |
| * Returns true if the action is enabled. |
| * |
| * @return true if the action is enabled, false otherwise |
| */ |
| public boolean isEnabled() |
| { |
| return (tree != null) && tree.isEnabled(); |
| } |
| } |
| |
| /** |
| * Returns true if the LookAndFeel implements the control icons. Package |
| * private for use in inner classes. |
| * |
| * @returns true if there are control icons |
| */ |
| boolean hasControlIcons() |
| { |
| if (expandedIcon != null || collapsedIcon != null) |
| return true; |
| return false; |
| } |
| |
| /** |
| * Returns control icon. It is null if the LookAndFeel does not implements the |
| * control icons. Package private for use in inner classes. |
| * |
| * @return control icon if it exists. |
| */ |
| Icon getCurrentControlIcon(TreePath path) |
| { |
| if (hasControlIcons()) |
| { |
| if (tree.isExpanded(path)) |
| return expandedIcon; |
| else |
| return collapsedIcon; |
| } |
| else |
| { |
| if (nullIcon == null) |
| nullIcon = new Icon() |
| { |
| public int getIconHeight() |
| { |
| return 0; |
| } |
| |
| public int getIconWidth() |
| { |
| return 0; |
| } |
| |
| public void paintIcon(Component c, Graphics g, int x, int y) |
| { |
| // No action here. |
| } |
| }; |
| return nullIcon; |
| } |
| } |
| |
| /** |
| * Returns the parent of the current node |
| * |
| * @param root is the root of the tree |
| * @param node is the current node |
| * @return is the parent of the current node |
| */ |
| Object getParent(Object root, Object node) |
| { |
| if (root == null || node == null || root.equals(node)) |
| return null; |
| |
| if (node instanceof TreeNode) |
| return ((TreeNode) node).getParent(); |
| return findNode(root, node); |
| } |
| |
| /** |
| * Recursively checks the tree for the specified node, starting at the root. |
| * |
| * @param root is starting node to start searching at. |
| * @param node is the node to search for |
| * @return the parent node of node |
| */ |
| private Object findNode(Object root, Object node) |
| { |
| if (! treeModel.isLeaf(root) && ! root.equals(node)) |
| { |
| int size = treeModel.getChildCount(root); |
| for (int j = 0; j < size; j++) |
| { |
| Object child = treeModel.getChild(root, j); |
| if (node.equals(child)) |
| return root; |
| |
| Object n = findNode(child, node); |
| if (n != null) |
| return n; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Selects the specified path in the tree depending on modes. Package private |
| * for use in inner classes. |
| * |
| * @param tree is the tree we are selecting the path in |
| * @param path is the path we are selecting |
| */ |
| void selectPath(JTree tree, TreePath path) |
| { |
| if (path != null) |
| { |
| tree.setSelectionPath(path); |
| tree.setLeadSelectionPath(path); |
| tree.makeVisible(path); |
| tree.scrollPathToVisible(path); |
| } |
| } |
| |
| /** |
| * Returns the path from node to the root. Package private for use in inner |
| * classes. |
| * |
| * @param node the node to get the path to |
| * @param depth the depth of the tree to return a path for |
| * @return an array of tree nodes that represent the path to node. |
| */ |
| Object[] getPathToRoot(Object node, int depth) |
| { |
| if (node == null) |
| { |
| if (depth == 0) |
| return null; |
| |
| return new Object[depth]; |
| } |
| |
| Object[] path = getPathToRoot(getParent(treeModel.getRoot(), node), |
| depth + 1); |
| path[path.length - depth - 1] = node; |
| return path; |
| } |
| |
| /** |
| * Draws a vertical line using the given graphic context |
| * |
| * @param g is the graphic context |
| * @param c is the component the new line will belong to |
| * @param x is the horizonal position |
| * @param top specifies the top of the line |
| * @param bottom specifies the bottom of the line |
| */ |
| protected void paintVerticalLine(Graphics g, JComponent c, int x, int top, |
| int bottom) |
| { |
| // FIXME: Check if drawing a dashed line or not. |
| g.setColor(getHashColor()); |
| g.drawLine(x, top, x, bottom); |
| } |
| |
| /** |
| * Draws a horizontal line using the given graphic context |
| * |
| * @param g is the graphic context |
| * @param c is the component the new line will belong to |
| * @param y is the vertical position |
| * @param left specifies the left point of the line |
| * @param right specifies the right point of the line |
| */ |
| protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, |
| int right) |
| { |
| // FIXME: Check if drawing a dashed line or not. |
| g.setColor(getHashColor()); |
| g.drawLine(left, y, right, y); |
| } |
| |
| /** |
| * Draws an icon at around a specific position |
| * |
| * @param c is the component the new line will belong to |
| * @param g is the graphic context |
| * @param icon is the icon which will be drawn |
| * @param x is the center position in x-direction |
| * @param y is the center position in y-direction |
| */ |
| protected void drawCentered(Component c, Graphics g, Icon icon, int x, int y) |
| { |
| x -= icon.getIconWidth() / 2; |
| y -= icon.getIconHeight() / 2; |
| |
| if (x < 0) |
| x = 0; |
| if (y < 0) |
| y = 0; |
| |
| icon.paintIcon(c, g, x, y); |
| } |
| |
| /** |
| * Draws a dashed horizontal line. |
| * |
| * @param g - the graphics configuration. |
| * @param y - the y location to start drawing at |
| * @param x1 - the x location to start drawing at |
| * @param x2 - the x location to finish drawing at |
| */ |
| protected void drawDashedHorizontalLine(Graphics g, int y, int x1, int x2) |
| { |
| g.setColor(getHashColor()); |
| for (int i = x1; i < x2; i += 2) |
| g.drawLine(i, y, i + 1, y); |
| } |
| |
| /** |
| * Draws a dashed vertical line. |
| * |
| * @param g - the graphics configuration. |
| * @param x - the x location to start drawing at |
| * @param y1 - the y location to start drawing at |
| * @param y2 - the y location to finish drawing at |
| */ |
| protected void drawDashedVerticalLine(Graphics g, int x, int y1, int y2) |
| { |
| g.setColor(getHashColor()); |
| for (int i = y1; i < y2; i += 2) |
| g.drawLine(x, i, x, i + 1); |
| } |
| |
| /** |
| * Paints the expand (toggle) part of a row. The receiver should NOT modify |
| * clipBounds, or insets. |
| * |
| * @param g - the graphics configuration |
| * @param clipBounds - |
| * @param insets - |
| * @param bounds - bounds of expand control |
| * @param path - path to draw control for |
| * @param row - row to draw control for |
| * @param isExpanded - is the row expanded |
| * @param hasBeenExpanded - has the row already been expanded |
| * @param isLeaf - is the path a leaf |
| */ |
| protected void paintExpandControl(Graphics g, Rectangle clipBounds, |
| Insets insets, Rectangle bounds, |
| TreePath path, int row, boolean isExpanded, |
| boolean hasBeenExpanded, boolean isLeaf) |
| { |
| if (shouldPaintExpandControl(path, row, isExpanded, hasBeenExpanded, isLeaf)) |
| { |
| Icon icon = getCurrentControlIcon(path); |
| int iconW = icon.getIconWidth(); |
| int x = bounds.x - iconW - gap; |
| icon.paintIcon(tree, g, x, bounds.y + bounds.height / 2 |
| - icon.getIconHeight() / 2); |
| } |
| } |
| |
| /** |
| * Paints the horizontal part of the leg. The receiver should NOT modify |
| * clipBounds, or insets. NOTE: parentRow can be -1 if the root is not |
| * visible. |
| * |
| * @param g - the graphics configuration |
| * @param clipBounds - |
| * @param insets - |
| * @param bounds - bounds of the cell |
| * @param path - path to draw leg for |
| * @param row - row to start drawing at |
| * @param isExpanded - is the row expanded |
| * @param hasBeenExpanded - has the row already been expanded |
| * @param isLeaf - is the path a leaf |
| */ |
| protected void paintHorizontalPartOfLeg(Graphics g, Rectangle clipBounds, |
| Insets insets, Rectangle bounds, |
| TreePath path, int row, |
| boolean isExpanded, |
| boolean hasBeenExpanded, |
| boolean isLeaf) |
| { |
| if (row != 0) |
| { |
| paintHorizontalLine(g, tree, bounds.y + bounds.height / 2, |
| bounds.x - leftChildIndent - gap, bounds.x - gap); |
| } |
| } |
| |
| /** |
| * Paints the vertical part of the leg. The receiver should NOT modify |
| * clipBounds, insets. |
| * |
| * @param g - the graphics configuration. |
| * @param clipBounds - |
| * @param insets - |
| * @param path - the path to draw the vertical part for. |
| */ |
| protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, |
| Insets insets, TreePath path) |
| { |
| Rectangle bounds = getPathBounds(tree, path); |
| TreePath parent = path.getParentPath(); |
| |
| boolean paintLine; |
| if (isRootVisible()) |
| paintLine = parent != null; |
| else |
| paintLine = parent != null && parent.getPathCount() > 1; |
| if (paintLine) |
| { |
| Rectangle parentBounds = getPathBounds(tree, parent); |
| paintVerticalLine(g, tree, parentBounds.x + 2 * gap, |
| parentBounds.y + parentBounds.height / 2, |
| bounds.y + bounds.height / 2); |
| } |
| } |
| |
| /** |
| * Paints the renderer part of a row. The receiver should NOT modify |
| * clipBounds, or insets. |
| * |
| * @param g - the graphics configuration |
| * @param clipBounds - |
| * @param insets - |
| * @param bounds - bounds of expand control |
| * @param path - path to draw control for |
| * @param row - row to draw control for |
| * @param isExpanded - is the row expanded |
| * @param hasBeenExpanded - has the row already been expanded |
| * @param isLeaf - is the path a leaf |
| */ |
| protected void paintRow(Graphics g, Rectangle clipBounds, Insets insets, |
| Rectangle bounds, TreePath path, int row, |
| boolean isExpanded, boolean hasBeenExpanded, |
| boolean isLeaf) |
| { |
| boolean selected = tree.isPathSelected(path); |
| boolean hasIcons = false; |
| Object node = path.getLastPathComponent(); |
| |
| paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, |
| hasBeenExpanded, isLeaf); |
| |
| TreeCellRenderer dtcr = currentCellRenderer; |
| |
| boolean focused = false; |
| if (treeSelectionModel != null) |
| focused = treeSelectionModel.getLeadSelectionRow() == row |
| && tree.isFocusOwner(); |
| |
| Component c = dtcr.getTreeCellRendererComponent(tree, node, selected, |
| isExpanded, isLeaf, row, |
| focused); |
| |
| rendererPane.paintComponent(g, c, c.getParent(), bounds); |
| } |
| |
| /** |
| * Prepares for the UI to uninstall. |
| */ |
| protected void prepareForUIUninstall() |
| { |
| // Nothing to do here yet. |
| } |
| |
| /** |
| * Returns true if the expand (toggle) control should be drawn for the |
| * specified row. |
| * |
| * @param path - current path to check for. |
| * @param row - current row to check for. |
| * @param isExpanded - true if the path is expanded |
| * @param hasBeenExpanded - true if the path has been expanded already |
| * @param isLeaf - true if the row is a lead |
| */ |
| protected boolean shouldPaintExpandControl(TreePath path, int row, |
| boolean isExpanded, |
| boolean hasBeenExpanded, |
| boolean isLeaf) |
| { |
| Object node = path.getLastPathComponent(); |
| return ! isLeaf && hasControlIcons(); |
| } |
| |
| /** |
| * Finish the editing session. |
| */ |
| void finish() |
| { |
| treeState.invalidatePathBounds(treeState.getPathForRow(editingRow)); |
| editingPath = null; |
| editingRow = - 1; |
| stopEditingInCompleteEditing = false; |
| isEditing = false; |
| Rectangle bounds = editingComponent.getParent().getBounds(); |
| tree.removeAll(); |
| validCachedPreferredSize = false; |
| // Repaint the region, where was the editing component. |
| tree.repaint(bounds); |
| editingComponent = null; |
| tree.requestFocus(); |
| } |
| |
| /** |
| * Returns the amount to indent the given row |
| * |
| * @return amount to indent the given row. |
| */ |
| protected int getRowX(int row, int depth) |
| { |
| return depth * totalChildIndent; |
| } |
| } // BasicTreeUI |