/*
* Copyright (c) 2000, Jacob Smullyan.
*
* This is part of SkunkDAV, a WebDAV client. See http://skunkdav.sourceforge.net/
* for the latest version.
*
* SkunkDAV 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.
*
* SkunkDAV 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 SkunkDAV; see the file COPYING. If not, write to the Free
* Software Foundation, 59 Temple Place - Suite 330, Boston, MA
* 02111-1307, USA.
*/
package org.skunk.swing;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Vector;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.skunk.trace.Debug;
/**
* a chooser that can be used as a filechooser,
* but which can display any tree of objects
*/
public class TreeNodeChooser extends JPanel
{
private TreeModel treeModel;
private DefaultComboBoxModel comboModel;
private SelectionMode selectionMode=SelectionMode.LEAF_ONLY;
//non-i18n defaults -- use mutators to customize
public static final String DEFAULT_ENTRY_LABEL="Selection: ";
public static final String DEFAULT_BRANCH_LABEL="Directories";
public static final String DEFAULT_LEAF_LABEL="Files";
private JTextField entryField;
private JLabel entryLabel, branchLabel, leafLabel;
private JList branchList, leafList;
private JComboBox nodeBox;
private String dialogTitle;
private TreePath selectedPath;
private String nodeSeparator=File.separator;
private boolean rootVisible=true;
private boolean entryFieldTextSticky=false;
/**
* constructs a TreeNodeChooser from a TreeModel
* @param treeModel the treeModel
*/
public TreeNodeChooser(TreeModel treeModel)
{
super();
this.treeModel=treeModel;
//by default start at the root node
setSelectedPath(new TreePath(treeModel.getRoot()));
initComponents();
}
/**
* returns the chooser's TreeModel
* @return the tree model
*/
public TreeModel getModel()
{
return this.treeModel;
}
/**
* Returns the separator used by the default renderer for the combo box
* in expressing the current TreePath as a file-path-like string.
* By default, nodeSeparator equals File.separator.
* If a custom renderer is added for the comboBox, this property may be ignored.
* @return the node separator
*/
public String getNodeSeparator()
{
return nodeSeparator;
}
/**
* sets the separator used by the default renderer for the combo box.
* @param nodeSeparator the new node separator
*/
public void setNodeSeparator(String nodeSeparator)
{
this.nodeSeparator=nodeSeparator;
}
/**
* indicates whether the text of the entry field is persistent
* when the chooser's directory is changed.
* @return whether the text field's value is sticky
*/
public boolean isEntryFieldTextSticky()
{
return this.entryFieldTextSticky;
}
/**
* determine whether the text of the entry field is persistent
* when the chooser's directory is changed.
* @param sticky whether the text field's value should be sticky
*/
public void setEntryFieldTextSticky(boolean sticky)
{
this.entryFieldTextSticky=sticky;
}
/**
* indicates whether the file name text field is editable
* @return whether the text field is editable
*/
public boolean isEntryFieldEditable()
{
return this.entryField.isEditable();
}
/**
* determine whether the file name text field is editable
* @param editable the editability of the text field
*/
public void setEntryFieldEditable(boolean editable)
{
this.entryField.setEditable(editable);
}
/**
* indicates whether the root of the tree model is visible in the chooser
* @return the visibility of the root node
*/
public boolean isRootVisible()
{
return this.rootVisible;
}
/**
* determine the visibility of the root node of the tree model
* @param rootVisible the visibility of the root node
*/
public void setRootVisible(boolean rootVisible)
{
Debug.trace(this, Debug.DP4, "setting rootVisible to " + rootVisible);
this.rootVisible=rootVisible;
if (!rootVisible)
{
Object root=treeModel.getRoot();
if (selectedPath.getLastPathComponent().equals(root))
{
//find first child of root that is not a leaf and set selected path to it
int childCnt=treeModel.getChildCount(root);
Debug.trace(this, Debug.DP4, "number of children of model root: "+childCnt);
for (int i=0;i<childCnt;i++)
{
Object o=treeModel.getChild(root, i);
if (!treeModel.isLeaf(o))
{
setCurrentPath(selectedPath.pathByAddingChild(o));
break;
}
else
{
Debug.trace(this, Debug.DP4, "found leaf under root node: "+ o);
}
}
}
}
}
/**
* returns the chooser's selection mode -- LEAF_ONLY, BRANCH_ONLY, or LEAF_AND_BRANCH
* @return the selection node
*/
public SelectionMode getSelectionMode()
{
return this.selectionMode;
}
/**
* sets the chooser's selection mode
* @param selectionMode the new selection mode
*/
public void setSelectionMode(SelectionMode selectionMode)
{
this.selectionMode=selectionMode;
}
/**
* sets the text of the label of the entry field.
* every internationalized application should set this,
* as the default is the English string "Selection: "
* @param entryLabelText the new text for the label
*/
public void setEntryLabelText(String entryLabelText)
{
this.entryLabel.setText(entryLabelText);
}
/**
* sets the text of the label of list of branch nodes.
* every internationalized application should set this,
* as the default is the English string "Directories: "
* @param branchLabelText the new text for the label
*/
public void setBranchLabelText(String branchLabelText)
{
this.branchLabel.setText(branchLabelText);
}
/**
* sets the text of the label of list of leaf nodes.
* every internationalized application should set this,
* as the default is the English string "Files: "
* @param leafLabelText the new text for the label
*/
public void setLeafLabelText(String leafLabelText)
{
this.leafLabel.setText(leafLabelText);
}
/**
* install a custom renderer for both list boxes.
* @param cellRenderer the new ListCellRenderer for the JLists
*/
public void setListCellRenderer(ListCellRenderer cellRenderer)
{
branchList.setCellRenderer(cellRenderer);
leafList.setCellRenderer(cellRenderer);
}
/**
* install a custom renderer for the combo box.
* @param cellRenderer the new ListCellRenderer for the JComboBox
*/
public void setComboBoxCellRenderer(ListCellRenderer cellRenderer)
{
nodeBox.setRenderer(cellRenderer);
}
/**
* returns the selected item in the JList of leaf nodes
* @return the selected item
*/
public Object getSelectedLeaf()
{
return leafList.getSelectedValue();
}
/**
* sets the selected item in the JList of leaf nodes
* @param leafObj the leaf node to select
*/
public void setSelectedLeaf(Object leafObj)
{
leafList.setSelectedValue(leafObj, true);
}
/**
* returns the selected item in the JList of branch nodes
* @return the selected item
*/
public Object getSelectedBranch()
{
return branchList.getSelectedValue();
}
/**
* sets the selected item in the JList of branch nodes
* @param branchObj the branch node to select
*/
public void setSelectedBranch(Object branchObj)
{
branchList.setSelectedValue(branchObj, true);
}
/**
* returns the selected path
* @return the selected path
*/
public TreePath getSelectedPath()
{
return selectedPath;
}
/**
* sets the selected path property,
* without adjusting the state of the JLists or combo box.
* @see setCurrentPath
* @param selectedPath the selected path
*/
public void setSelectedPath(TreePath selectedPath)
{
this.selectedPath=selectedPath;
}
/**
* sets the selected path and displays it in the chooser
* @param currentPath the new path to display
*/
public void setCurrentPath(TreePath currentPath)
{
Debug.trace(this, Debug.DP4, "in setCurrentPath({0})", currentPath);
setSelectedPath(currentPath);
setComboPath();
Debug.trace(this, Debug.DP5, "about to set list path");
setListPath();
}
/**
* sets the text of the entry field
* @param text the text for the entry field
*/
public void setEntryFieldText(String text)
{
this.entryField.setText(text);
}
/**
* returns the text of the entry field
* @return the entry field text
*/
public String getEntryFieldText()
{
return this.entryField.getText();
}
private void initComponents()
{
//shows the current node
nodeBox=createNodeBox();
//shows those children of the current node which are not leaves
branchList=new JList();
//shows those children of the current node which are leaves
leafList=new JList();
//shows the current selection
entryField=new JTextField(30);
//label for branchList
branchLabel=new JLabel(DEFAULT_BRANCH_LABEL);
//label for leafList
leafLabel=new JLabel(DEFAULT_LEAF_LABEL);
//label for entryField
entryLabel=new JLabel(DEFAULT_ENTRY_LABEL);
//wire them together
initListeners(nodeBox,
branchList,
leafList,
entryLabel,
entryField);
initLayout(nodeBox,
branchLabel,
branchList,
leafLabel,
leafList,
entryLabel,
entryField);
setListPath();
}
private JComboBox createNodeBox()
{
comboModel=new DefaultComboBoxModel();
setComboPath();
JComboBox combo=new JComboBox(comboModel);
combo.setRenderer(new ComboRenderer());
return combo;
}
/**
* sets the comboBox model to show the current node and its ancestors.
* if the rootVisible property is false, all the root node's children, regardless
* of whether they are ancestors of the current node, are shown, as otherwise
* there is no way to navigate to them.
*/
private void setComboPath()
{
Debug.trace(this, Debug.DP5, "in setComboPath");
comboModel.removeAllElements();
Object[] pathObjs=selectedPath.getPath();
Debug.trace(this, Debug.DP5, "selectedPath: {0}", selectedPath);
if (!isRootVisible())
{
Debug.trace(this, Debug.DP5, "root is not visible");
Object root=treeModel.getRoot();
for (int i=0;i<treeModel.getChildCount(root);i++)
{
Object subRoot=treeModel.getChild(root, i);
if (!treeModel.isLeaf(subRoot))
{
comboModel.addElement(subRoot);
Debug.trace(this, Debug.DP5, "adding subRoot "+ subRoot);
if (pathObjs.length>2 && pathObjs[1].equals(subRoot))
{
Debug.trace(this, Debug.DP5, "path contains subRoot!");
for (int j=2;j<pathObjs.length;j++)
{
comboModel.addElement(pathObjs[j]);
}
}
}
}
}
else
{
Debug.trace(this, Debug.DP5, "root is visible");
for (int i=0;i<pathObjs.length;i++)
{
comboModel.addElement(pathObjs[i]);
}
}
comboModel.setSelectedItem(pathObjs[pathObjs.length-1]);
Debug.trace(this, Debug.DP5, "comboModel: {0}", comboModel);
}
private void setListPath()
{
Debug.trace(this, Debug.DP5, "in setListPath()");
Vector branchVector=new Vector();
Vector leafVector=new Vector();
TreeNode parent=(TreeNode)getSelectedPath().getLastPathComponent();
if (parent==null)
{
Debug.trace(this, Debug.DP2, "selected path {0} is funky", getSelectedPath());
return;
}
int childCnt=treeModel.getChildCount(parent);
for (int i=0;i<childCnt;i++)
{
TreeNode kid=(TreeNode)treeModel.getChild(parent, i);
if (kid.isLeaf())
{
leafVector.addElement(kid);
}
else
{
branchVector.addElement(kid);
}
}
branchList.setListData(branchVector);
leafList.setListData(leafVector);
}
/**
* lays out the components.
* I'm emulating the GtkFileSelection widget,
* but at present leaving out certain features I don't need,
* like the buttons to create/delete/rename
*/
private void initLayout(JComboBox nodeBox,
JLabel branchLabel,
JList branchList,
JLabel leafLabel,
JList leafList,
JLabel entryLabel,
JTextField entryField)
{
this.setLayout(new GridBagLayout());
GridBagConstraints gbc=new GridBagConstraints();
gbc.gridx=0;
gbc.gridy=0;
gbc.gridheight=1;
gbc.gridwidth=2;
gbc.fill=GridBagConstraints.BOTH;
gbc.insets=new Insets(2, 2, 2, 2);
gbc.anchor=GridBagConstraints.CENTER;
this.add(nodeBox, gbc);
gbc.gridy++;
gbc.gridwidth=1;
gbc.anchor=GridBagConstraints.WEST;
this.add(branchLabel, gbc);
gbc.gridx++;
this.add(leafLabel, gbc);
gbc.gridx=0;
gbc.gridy++;
Dimension d=new Dimension(180, 360);
JScrollPane tmpPane=new JScrollPane(branchList);
tmpPane.setPreferredSize(d);
this.add(tmpPane, gbc);
gbc.gridx++;
tmpPane=new JScrollPane(leafList);
tmpPane.setPreferredSize(d);
this.add(tmpPane, gbc);
gbc.gridx=0;
gbc.gridy++;
gbc.gridwidth=2;
this.add(entryLabel, gbc);
gbc.gridy++;
this.add(entryField, gbc);
}
private void initListeners(JComboBox nodeBox,
JList branchList,
JList leafList,
JLabel entryLabel,
JTextField entryField)
{
nodeBox.addActionListener(new ComboActionListener());
branchList.addListSelectionListener(new BranchListSelectionListener());
branchList.addMouseListener(new BranchListMouseListener());
leafList.addListSelectionListener(new LeafListSelectionListener());
}
private Object[] getComboPath(Object value)
{
Debug.trace(this, Debug.DP5, "in getComboPath{0}", new Object[] {value});
if (isRootVisible())
{
int size=comboModel.getIndexOf(value)+1;
Object[] path=new Object[size];
for (int i=0;i<size;i++)
{
path[i]=comboModel.getElementAt(i);
}
return path;
}
else
{
Debug.trace(this, Debug.DP5, "in getComboPath, invisible root");
//a little more complicated, as the non-ancestor modes need to be eliminated.
Object selectedItem=comboModel.getSelectedItem();
int itemCount=comboModel.getSize();
//gather the tree's subroots
Object treeRoot=treeModel.getRoot();
int subRootCount=treeModel.getChildCount(treeRoot);
ArrayList subRoots=new ArrayList(subRootCount);
for (int i=0;i<subRootCount;i++)
{
subRoots.add(treeModel.getChild(treeRoot, i));
}
Debug.trace(this, Debug.DP5, "subRoots: {0}", subRoots);
ArrayList pathList=new ArrayList();
//iterate through items in comboModel. if it is a subroot, set it
//as the first element in the path, eliminating whatever else is there.
//if equal to selected item, break. else
for (int i=0;i<itemCount;i++)
{
Object nextItem=comboModel.getElementAt(i);
if (subRoots.contains(nextItem))
{
//wipe out pathList if we have a subroot, in effect
//eliminating any previous subroot from the reported path
pathList.clear();
}
pathList.add(nextItem);
if (nextItem.equals(selectedItem))
break;
}
//insert root node
pathList.add(0, treeRoot);
Debug.trace(this, Debug.DP5, "returning from getComboPath(): "+pathList);
return pathList.toArray();
}
}
protected class ComboRenderer extends DefaultListCellRenderer
{
protected ComboRenderer()
{
super();
}
public Component getListCellRendererComponent(JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus)
{
//from value we produce the corresponding treepath, then represent it
//in a path-like fashion.
return super.getListCellRendererComponent(list,
formatPath(getComboPath(value)),
index,
isSelected,
cellHasFocus);
}
protected String formatPath(Object[] path)
{
StringBuffer buffer=new StringBuffer();
for (int i=0;i<path.length;i++)
{
if (i==0 && !isRootVisible())
continue;
buffer.append(path[i])
.append(getNodeSeparator());
}
return buffer.toString();
}
}
private class ComboActionListener implements ActionListener
{
public void actionPerformed(ActionEvent ae)
{
DefaultComboBoxModel model=TreeNodeChooser.this.comboModel;
Object selectedItem=model.getSelectedItem();
int index=model.getIndexOf(selectedItem);
if (index>=0)
{
int offset=(isRootVisible())
? 0
: 1;
int pathSize=index + 1 + offset;
Object[] path=new Object[pathSize];
synchronized(model)
{
if (!isRootVisible())
path[0]=getModel().getRoot();
// note that I am looping over a collection
// from which I am also removing elements,
// normally not such a good thing.
for (int i=0;i<model.getSize();i++)
{
if (i+offset<path.length)
{
path[i+offset]=model.getElementAt(i);
}
else
{
model.removeElementAt(i);
}
}
}
setSelectedPath(new TreePath(path));
setListPath();
}
}
}
private class BranchListSelectionListener implements ListSelectionListener
{
public void valueChanged(ListSelectionEvent lassie)
{
if (!isEntryFieldTextSticky())
{
TreeNodeChooser.this.entryField.setText("");
if (!selectionMode.equals(SelectionMode.LEAF_ONLY))
{
Object branchValue=TreeNodeChooser.this.branchList.getSelectedValue();
if (branchValue!=null )
TreeNodeChooser.this.entryField.setText(branchValue.toString());
}
}
}
}
private class BranchListMouseListener extends MouseAdapter
{
public void mouseClicked(MouseEvent eek)
{
if (eek.getClickCount()==2)
{
JList daList=TreeNodeChooser.this.branchList;
int index=daList.locationToIndex(eek.getPoint());
if (index<0)
{
Debug.trace(this, Debug.DP5, "JList mouse event reports negative index: "+index);
return;
}
Object branch=daList.getModel().getElementAt(index);
setCurrentPath(getSelectedPath().pathByAddingChild(branch));
}
}
}
private class LeafListSelectionListener implements ListSelectionListener
{
public void valueChanged(ListSelectionEvent lassie)
{
if (!selectionMode.equals(SelectionMode.BRANCH_ONLY))
{
Object leafValue=TreeNodeChooser.this.leafList.getSelectedValue();
if (leafValue!=null)
TreeNodeChooser.this.entryField.setText(leafValue.toString());
}
}
}
/**
* an enumerated type to represent chooser selection modes
*/
public static final class SelectionMode
{
/**
* Only leaves can be selected
*/
public static final SelectionMode LEAF_ONLY=new SelectionMode("leaf_only");
/**
* Both leaves and branches can be selected
*/
public static final SelectionMode LEAF_AND_BRANCH=new SelectionMode("leaf_and_branch");
/**
* Only branches can be selected
*/
public static final SelectionMode BRANCH_ONLY=new SelectionMode("branch_only");
private String s;
private SelectionMode(String s)
{
this.s=s;
}
public String toString()
{
return s;
}
}
}
/* $Log: TreeNodeChooser.java,v $
/* Revision 1.4 2001/01/04 06:02:49 smulloni
/* added more javadoc documentation.
/*
/* Revision 1.3 2001/01/03 20:11:31 smulloni
/* the DAVFileChooser now replaces JFileChooser for remote file access.
/* DAVMethod now has a protocol property.
/*
/* Revision 1.2 2001/01/03 00:30:35 smulloni
/* a number of modifications along the way to replacing JFileChooser with
/* something more suitable for remote (virtual) files.
/*
/* Revision 1.1 2001/01/02 15:53:51 smulloni
/* draft of filechooser
/* */
|