/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Intersect Software Corporation This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.ui.swing.components; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import java.util.Stack; import java.util.Vector; import javax.swing.ComboBoxModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JToolBar; import javax.swing.JTree; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.UIManager; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import net.wotonomy.foundation.internal.WotonomyException; /** * TreeChooser is a FileChooser-like panel that uses a TreeModel as a data * source. It basically provides an alternative to JTree for rendering and * manipulating tree-like data. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 904 $ */ public class TreeChooser extends JPanel implements ActionListener, ListSelectionListener, TreeSelectionListener, TreeModelListener, ListCellRenderer { /** * The TreeChooser responds to this action command by calling displayPrevious(). */ public static final String BACK = "Back"; /** * The TreeChooser responds to this action command by calling displayHome(). */ public static final String HOME = "Home"; /** * The TreeChooser responds to this action command by calling displayParent(). */ public static final String UP = "Up"; /** * The TreeChooser responds to this action command by attempting to navigate to * the first node in the current selection and display that node's children. */ public static final String SELECT = "Select"; protected JList contents; protected JComboBox pathCombo; protected JToolBar toolBar; protected TreeModel model; protected TreeSelectionModel selectionModel; protected TreeCellRenderer renderer; protected TreePath displayPath; protected Stack pathStack; protected int pathIndent; private ChooserComboBoxModel comboBoxModel; private JTree bogusJTree; // needed for tree cell renderer private Dimension preferredSize; public TreeChooser() { preferredSize = new Dimension(300, 200); model = new DefaultTreeModel(new DefaultMutableTreeNode("Root")); displayPath = new TreePath(model.getRoot()); selectionModel = new DefaultTreeSelectionModel(); renderer = new DefaultTreeCellRenderer(); pathStack = new Stack(); pathIndent = 0; // 16; comboBoxModel = new ChooserComboBoxModel(this); bogusJTree = new JTree(); bogusJTree.setModel(model); init(); displayHome(); stopListening(); // clear existing listeners startListening(); } public Dimension getPreferredSize() { return preferredSize; } protected void init() { this.setLayout(new BorderLayout(10, 10)); contents = initList(); contents.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); // synchs with DefaultTreeSelectionModel JScrollPane scrollPane = new JScrollPane(contents); scrollPane.setPreferredSize(new Dimension(200, 150)); this.add(scrollPane, BorderLayout.CENTER); Component previewPane = initPreviewPane(); if (previewPane != null) { this.add(previewPane, BorderLayout.EAST); } JPanel navigationPanel = new JPanel(); navigationPanel.setLayout(new BorderLayout(10, 10)); this.add(navigationPanel, BorderLayout.NORTH); pathCombo = initComboBox(); if (pathCombo != null) { pathCombo.setModel(comboBoxModel); // put combo in a grid bag to handle varying // heights of JToolBars across platforms JPanel panel = new JPanel(); panel.setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0; panel.add(pathCombo, gbc); navigationPanel.add(panel, BorderLayout.CENTER); } Component toolBar = initToolBar(); if (toolBar != null) { navigationPanel.add(toolBar, BorderLayout.EAST); } } /** * Creates tool bar or return null if no tool bar is desired. This * implementation returns a JToolBar containing buttons for BACK, UP, and HOME. */ protected Component initToolBar() { JToolBar toolBar = new JToolBar(); toolBar.setFloatable(false); JButton button; button = new JButton(UIManager.getIcon("FileChooser.upFolderIcon")); button.setActionCommand(UP); button.addActionListener(this); toolBar.add(button); button = new JButton(UIManager.getIcon("FileChooser.homeFolderIcon")); button.setActionCommand(HOME); button.addActionListener(this); toolBar.add(button); /* * button = new JButton( UIManager.getIcon("FileChooser.newFolderIcon") ); * button.setActionCommand( BACK ); button.addActionListener( this ); * toolBar.add( button ); */ return toolBar; } /** * Creates the component that is used to display a preview of the selected * item(s) in the content area. This component would listen to the selection * model to update itself when the selected items change. Return null to omit * this component. This implementation returns null. */ protected Component initPreviewPane() { return null; } /** * Creates the JComboBox that is used to render the path leading to the * displayed contents. Return null to omit this combo box. This implementation * returns a stock JComboBox that uses this class as its cell renderer. */ protected JComboBox initComboBox() { JComboBox comboBox = new JComboBox(); comboBox.setRenderer(this); return comboBox; } /** * Creates the JList that is used to render the path leading to the displayed * contents. This method may not return null. This implementation returns a * stock JList that uses this class as its cell renderer and fires a SELECT * action event on double click. */ protected JList initList() { JList list = new JList(); list.setCellRenderer(this); list.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent evt) { if (evt.getClickCount() > 1) { actionPerformed(new ActionEvent(this, 0, SELECT)); } } }); return list; } /** * Begins listening to the specified tree model and tree selection model. */ protected void startListening() { model.addTreeModelListener(this); selectionModel.addTreeSelectionListener(this); contents.addListSelectionListener(this); } /** * Stops listening to the specified tree model and tree selection model. */ protected void stopListening() { model.removeTreeModelListener(this); selectionModel.removeTreeSelectionListener(this); contents.removeListSelectionListener(this); } /** * Returns the TreeModel used by the TreeChooser. */ public TreeModel getModel() { return model; } /** * Sets the TreeModel used by the TreeChooser. */ public void setModel(TreeModel aTreeModel) { stopListening(); model = aTreeModel; bogusJTree.setModel(aTreeModel); pathStack.removeAllElements(); startListening(); displayHome(); } /** * Returns the TreeSelectionModel used by the TreeChooser. */ public TreeSelectionModel getSelectionModel() { return selectionModel; } /** * Sets the TreeSelectionModel used by the TreeChooser. */ public void setSelectionModel(TreeSelectionModel aSelectionModel) { selectionModel = aSelectionModel; if (aSelectionModel.getSelectionMode() == TreeSelectionModel.SINGLE_TREE_SELECTION) { contents.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); } else { contents.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); } updateSelection(); } /** * Returns the TreeCellRenderer used by the TreeChooser. */ public TreeCellRenderer getRenderer() { return renderer; } /** * Sets the TreeCellRenderer used by the TreeChooser. */ public void setRenderer(TreeCellRenderer aRenderer) { renderer = aRenderer; updateContents(); } /** * Displays the "home" directory. This implementation displays the root node's * children. */ public void displayHome() { setDisplayPath(null); } /** * Displays the parent path of the currently displayed path. */ public void displayParent() { setDisplayPath(displayPath.getParentPath()); } /** * Displays the last displayed path before the current one, emulating the * behavior of a "back" button. */ public void displayPrevious() { if (pathStack.empty()) { displayHome(); } else { setDisplayPathDirect((TreePath) pathStack.pop()); updateContents(); } } /** * Pushes the previous item onto the stack, sets the display path, and then * updates the contents. If aPath is null, the root node's children are * displayed. */ public void setDisplayPath(TreePath aPath) { if (aPath == null) { aPath = new TreePath(getModel().getRoot()); } if (!displayPath.equals(aPath)) { pathStack.push(displayPath); setDisplayPathDirect(aPath); } updateContents(); } /** * Sets the displayPath field and does not update the stack nor update the * contents. */ protected void setDisplayPathDirect(TreePath aPath) { displayPath = aPath; } /** * Gets the currently displayed path. */ public TreePath getDisplayPath() { return displayPath; } /** * Called when selected path changes or when model indicates that the displayed * path has changed. */ protected void updateContents() { stopListening(); // update combo box comboBoxModel.fireContentsChanged(); // update list contents Object displayedObject = displayPath.getLastPathComponent(); /* * //FIXME: this display group doesn't seem to be getting the sort orderings * from parent if ( displayedObject instanceof net.wotonomy.ui.EODisplayGroup ) * System.out.println( * ((net.wotonomy.ui.EODisplayGroup)displayedObject).displayedObjects() ); */ int count = model.getChildCount(displayedObject); Object[] children = new Object[count]; for (int i = 0; i < count; i++) { children[i] = model.getChild(displayedObject, i); } contents.setListData(children); startListening(); // synchronize the selection updateSelection(); } /** * Updates the selection in the list to reflect the selection in the tree * selection model. */ public void updateSelection() { int index; Object last = displayPath.getLastPathComponent(); TreePath[] selectionPaths = selectionModel.getSelectionPaths(); if (selectionPaths != null) { List selectedIndices = new LinkedList(); for (int i = 0; i < selectionPaths.length; i++) { if (displayPath.equals(selectionPaths[i].getParentPath())) { index = getModel().getIndexOfChild(last, selectionPaths[i].getLastPathComponent()); if (index != -1) { selectedIndices.add(new Integer(index)); } else // should never happen { throw new WotonomyException("Could not find child of displayed node."); } } } int[] selected = new int[selectedIndices.size()]; for (int i = 0; i < selected.length; i++) { selected[i] = ((Integer) selectedIndices.get(i)).intValue(); } stopListening(); contents.setSelectedIndices(selected); startListening(); } } // interface TreeModelListener public void treeNodesChanged(TreeModelEvent evt) { /* * if ( displayPath.getLastPathComponent().toString().equals( * evt.getTreePath().getLastPathComponent().toString() ) ) { System.out.println( * "TreeChooser.treeNodesChanged: " + count++ ); */ updateContents(); /* * } else { System.out.println( evt.getTreePath() + " != " + displayPath ); } */ } public void treeNodesInserted(TreeModelEvent evt) { // updateContents(); } public void treeNodesRemoved(TreeModelEvent evt) { // updateContents(); } public void treeStructureChanged(TreeModelEvent evt) { if ((evt.getTreePath().equals(displayPath)) || (evt.getTreePath().isDescendant(displayPath))) { // setDisplayPath( evt.getTreePath() ); } displayHome(); } // interface TreeSelectionListener /** * Called when the tree selection model's value changes. This is presumably an * external change, so this calls updateSelection. */ public void valueChanged(TreeSelectionEvent evt) { updateSelection(); } // interface ListSelectionListener /** * Called when user changes the selection in the list. This implementation * updates the tree selection model with the corresponding selection. */ public void valueChanged(ListSelectionEvent evt) { if (!evt.getValueIsAdjusting()) { Object last = displayPath.getLastPathComponent(); int[] selection = contents.getSelectedIndices(); TreePath[] selectionPaths = new TreePath[selection.length]; for (int i = 0; i < selection.length; i++) { selectionPaths[i] = displayPath.pathByAddingChild(getModel().getChild(last, selection[i])); } selectionModel.setSelectionPaths(selectionPaths); } } // interface ListCellRenderer /** * This method returns the component returned by the tree cell renderer. */ public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { boolean isLeaf = (model.isLeaf(value)); bogusJTree.setForeground(list.getForeground()); bogusJTree.setBackground(list.getBackground()); JComponent result = (JComponent) renderer.getTreeCellRendererComponent(bogusJTree, value, isSelected, (list != contents), isLeaf, index, cellHasFocus); /* * if ( ( list != contents ) && ( index > -1 ) ) { result.setBorder( * BorderFactory.createEmptyBorder( 0, index*pathIndent, 0, 0 ) ); } else { * result.setBorder( BorderFactory.createEmptyBorder() ); } */ return result; } // interface ActionListener public void actionPerformed(ActionEvent evt) { String command = evt.getActionCommand(); if (HOME.equals(command)) { displayHome(); } else if (UP.equals(command)) { displayParent(); } else if (BACK.equals(command)) { displayPrevious(); } else if (SELECT.equals(command)) { Cursor oldCursor = getCursor(); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); int index = contents.getSelectedIndex(); // if selection if (index != -1) { Object parent = displayPath.getLastPathComponent(); Object child = getModel().getChild(parent, index); // if selected item is not a leaf if (getModel().getChildCount(child) > 0) { // navigate to selected item setDisplayPath(displayPath.pathByAddingChild(child)); } } setCursor(oldCursor); } } private class ChooserComboBoxModel implements ComboBoxModel { TreeChooser treeChooser; Vector listeners; ChooserComboBoxModel(TreeChooser aTreeChooser) { treeChooser = aTreeChooser; listeners = new Vector(); } public int getSize() { return treeChooser.displayPath.getPathCount(); } public Object getElementAt(int index) { return treeChooser.displayPath.getPathComponent(index); } public Object getSelectedItem() { return treeChooser.displayPath.getLastPathComponent(); } public void setSelectedItem(Object anItem) { if (!(treeChooser.displayPath.getLastPathComponent().equals(anItem))) { Object[] items = treeChooser.displayPath.getPath(); TreePath path = new TreePath(getModel().getRoot()); for (int i = 1; i < items.length; i++) { if (path.getLastPathComponent() == anItem) { treeChooser.setDisplayPath(path); return; } path = path.pathByAddingChild(items[i]); } } } public void addListDataListener(ListDataListener l) { listeners.add(l); } public void removeListDataListener(ListDataListener l) { listeners.remove(l); } public void fireContentsChanged() { Enumeration e = listeners.elements(); while (e.hasMoreElements()) { ((ListDataListener) e.nextElement()) .contentsChanged(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, getSize())); } } } }