TreeNodeChooser.java :  » Net » SkunkDAV » org » skunk » swing » Java Open Source

Java Open Source » Net » SkunkDAV 
SkunkDAV » org » skunk » swing » TreeNodeChooser.java
/*
 *  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
/* */
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.