001: /*
002: * Copyright (c) 2000, Jacob Smullyan.
003: *
004: * This is part of SkunkDAV, a WebDAV client. See http://skunkdav.sourceforge.net/
005: * for the latest version.
006: *
007: * SkunkDAV is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License as published
009: * by the Free Software Foundation; either version 2, or (at your option)
010: * any later version.
011: *
012: * SkunkDAV is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with SkunkDAV; see the file COPYING. If not, write to the Free
019: * Software Foundation, 59 Temple Place - Suite 330, Boston, MA
020: * 02111-1307, USA.
021: */
022:
023: package org.skunk.swing;
024:
025: import java.awt.BorderLayout;
026: import java.awt.Component;
027: import java.awt.Dimension;
028: import java.awt.GridBagConstraints;
029: import java.awt.GridBagLayout;
030: import java.awt.Insets;
031: import java.awt.event.ActionEvent;
032: import java.awt.event.ActionListener;
033: import java.awt.event.MouseAdapter;
034: import java.awt.event.MouseEvent;
035: import java.io.File;
036: import java.util.ArrayList;
037: import java.util.Collections;
038: import java.util.Enumeration;
039: import java.util.Vector;
040: import javax.swing.DefaultComboBoxModel;
041: import javax.swing.DefaultListCellRenderer;
042: import javax.swing.JComboBox;
043: import javax.swing.JLabel;
044: import javax.swing.JList;
045: import javax.swing.JPanel;
046: import javax.swing.JScrollPane;
047: import javax.swing.JTextField;
048: import javax.swing.ListCellRenderer;
049: import javax.swing.event.ListSelectionEvent;
050: import javax.swing.event.ListSelectionListener;
051: import javax.swing.tree.TreeModel;
052: import javax.swing.tree.TreeNode;
053: import javax.swing.tree.TreePath;
054: import org.skunk.trace.Debug;
055:
056: /**
057: * a chooser that can be used as a filechooser,
058: * but which can display any tree of objects
059: */
060: public class TreeNodeChooser extends JPanel {
061: private TreeModel treeModel;
062: private DefaultComboBoxModel comboModel;
063: private SelectionMode selectionMode = SelectionMode.LEAF_ONLY;
064:
065: //non-i18n defaults -- use mutators to customize
066: public static final String DEFAULT_ENTRY_LABEL = "Selection: ";
067: public static final String DEFAULT_BRANCH_LABEL = "Directories";
068: public static final String DEFAULT_LEAF_LABEL = "Files";
069:
070: private JTextField entryField;
071: private JLabel entryLabel, branchLabel, leafLabel;
072: private JList branchList, leafList;
073: private JComboBox nodeBox;
074: private String dialogTitle;
075: private TreePath selectedPath;
076: private String nodeSeparator = File.separator;
077: private boolean rootVisible = true;
078: private boolean entryFieldTextSticky = false;
079:
080: /**
081: * constructs a TreeNodeChooser from a TreeModel
082: * @param treeModel the treeModel
083: */
084: public TreeNodeChooser(TreeModel treeModel) {
085: super ();
086: this .treeModel = treeModel;
087: //by default start at the root node
088: setSelectedPath(new TreePath(treeModel.getRoot()));
089: initComponents();
090: }
091:
092: /**
093: * returns the chooser's TreeModel
094: * @return the tree model
095: */
096: public TreeModel getModel() {
097: return this .treeModel;
098: }
099:
100: /**
101: * Returns the separator used by the default renderer for the combo box
102: * in expressing the current TreePath as a file-path-like string.
103: * By default, nodeSeparator equals File.separator.
104: * If a custom renderer is added for the comboBox, this property may be ignored.
105: * @return the node separator
106: */
107: public String getNodeSeparator() {
108: return nodeSeparator;
109: }
110:
111: /**
112: * sets the separator used by the default renderer for the combo box.
113: * @param nodeSeparator the new node separator
114: */
115: public void setNodeSeparator(String nodeSeparator) {
116: this .nodeSeparator = nodeSeparator;
117: }
118:
119: /**
120: * indicates whether the text of the entry field is persistent
121: * when the chooser's directory is changed.
122: * @return whether the text field's value is sticky
123: */
124: public boolean isEntryFieldTextSticky() {
125: return this .entryFieldTextSticky;
126: }
127:
128: /**
129: * determine whether the text of the entry field is persistent
130: * when the chooser's directory is changed.
131: * @param sticky whether the text field's value should be sticky
132: */
133: public void setEntryFieldTextSticky(boolean sticky) {
134: this .entryFieldTextSticky = sticky;
135: }
136:
137: /**
138: * indicates whether the file name text field is editable
139: * @return whether the text field is editable
140: */
141: public boolean isEntryFieldEditable() {
142: return this .entryField.isEditable();
143: }
144:
145: /**
146: * determine whether the file name text field is editable
147: * @param editable the editability of the text field
148: */
149: public void setEntryFieldEditable(boolean editable) {
150: this .entryField.setEditable(editable);
151: }
152:
153: /**
154: * indicates whether the root of the tree model is visible in the chooser
155: * @return the visibility of the root node
156: */
157: public boolean isRootVisible() {
158: return this .rootVisible;
159: }
160:
161: /**
162: * determine the visibility of the root node of the tree model
163: * @param rootVisible the visibility of the root node
164: */
165: public void setRootVisible(boolean rootVisible) {
166: Debug.trace(this , Debug.DP4, "setting rootVisible to "
167: + rootVisible);
168: this .rootVisible = rootVisible;
169: if (!rootVisible) {
170: Object root = treeModel.getRoot();
171: if (selectedPath.getLastPathComponent().equals(root)) {
172: //find first child of root that is not a leaf and set selected path to it
173: int childCnt = treeModel.getChildCount(root);
174: Debug
175: .trace(this , Debug.DP4,
176: "number of children of model root: "
177: + childCnt);
178: for (int i = 0; i < childCnt; i++) {
179: Object o = treeModel.getChild(root, i);
180: if (!treeModel.isLeaf(o)) {
181: setCurrentPath(selectedPath
182: .pathByAddingChild(o));
183: break;
184: } else {
185: Debug.trace(this , Debug.DP4,
186: "found leaf under root node: " + o);
187: }
188: }
189: }
190: }
191: }
192:
193: /**
194: * returns the chooser's selection mode -- LEAF_ONLY, BRANCH_ONLY, or LEAF_AND_BRANCH
195: * @return the selection node
196: */
197: public SelectionMode getSelectionMode() {
198: return this .selectionMode;
199: }
200:
201: /**
202: * sets the chooser's selection mode
203: * @param selectionMode the new selection mode
204: */
205: public void setSelectionMode(SelectionMode selectionMode) {
206: this .selectionMode = selectionMode;
207: }
208:
209: /**
210: * sets the text of the label of the entry field.
211: * every internationalized application should set this,
212: * as the default is the English string "Selection: "
213: * @param entryLabelText the new text for the label
214: */
215: public void setEntryLabelText(String entryLabelText) {
216: this .entryLabel.setText(entryLabelText);
217: }
218:
219: /**
220: * sets the text of the label of list of branch nodes.
221: * every internationalized application should set this,
222: * as the default is the English string "Directories: "
223: * @param branchLabelText the new text for the label
224: */
225: public void setBranchLabelText(String branchLabelText) {
226: this .branchLabel.setText(branchLabelText);
227: }
228:
229: /**
230: * sets the text of the label of list of leaf nodes.
231: * every internationalized application should set this,
232: * as the default is the English string "Files: "
233: * @param leafLabelText the new text for the label
234: */
235: public void setLeafLabelText(String leafLabelText) {
236: this .leafLabel.setText(leafLabelText);
237: }
238:
239: /**
240: * install a custom renderer for both list boxes.
241: * @param cellRenderer the new ListCellRenderer for the JLists
242: */
243: public void setListCellRenderer(ListCellRenderer cellRenderer) {
244: branchList.setCellRenderer(cellRenderer);
245: leafList.setCellRenderer(cellRenderer);
246: }
247:
248: /**
249: * install a custom renderer for the combo box.
250: * @param cellRenderer the new ListCellRenderer for the JComboBox
251: */
252: public void setComboBoxCellRenderer(ListCellRenderer cellRenderer) {
253: nodeBox.setRenderer(cellRenderer);
254: }
255:
256: /**
257: * returns the selected item in the JList of leaf nodes
258: * @return the selected item
259: */
260: public Object getSelectedLeaf() {
261: return leafList.getSelectedValue();
262: }
263:
264: /**
265: * sets the selected item in the JList of leaf nodes
266: * @param leafObj the leaf node to select
267: */
268: public void setSelectedLeaf(Object leafObj) {
269: leafList.setSelectedValue(leafObj, true);
270: }
271:
272: /**
273: * returns the selected item in the JList of branch nodes
274: * @return the selected item
275: */
276: public Object getSelectedBranch() {
277: return branchList.getSelectedValue();
278: }
279:
280: /**
281: * sets the selected item in the JList of branch nodes
282: * @param branchObj the branch node to select
283: */
284: public void setSelectedBranch(Object branchObj) {
285: branchList.setSelectedValue(branchObj, true);
286: }
287:
288: /**
289: * returns the selected path
290: * @return the selected path
291: */
292: public TreePath getSelectedPath() {
293: return selectedPath;
294: }
295:
296: /**
297: * sets the selected path property,
298: * without adjusting the state of the JLists or combo box.
299: * @see setCurrentPath
300: * @param selectedPath the selected path
301: */
302: public void setSelectedPath(TreePath selectedPath) {
303: this .selectedPath = selectedPath;
304: }
305:
306: /**
307: * sets the selected path and displays it in the chooser
308: * @param currentPath the new path to display
309: */
310: public void setCurrentPath(TreePath currentPath) {
311: Debug.trace(this , Debug.DP4, "in setCurrentPath({0})",
312: currentPath);
313: setSelectedPath(currentPath);
314: setComboPath();
315: Debug.trace(this , Debug.DP5, "about to set list path");
316: setListPath();
317: }
318:
319: /**
320: * sets the text of the entry field
321: * @param text the text for the entry field
322: */
323: public void setEntryFieldText(String text) {
324: this .entryField.setText(text);
325: }
326:
327: /**
328: * returns the text of the entry field
329: * @return the entry field text
330: */
331: public String getEntryFieldText() {
332: return this .entryField.getText();
333: }
334:
335: private void initComponents() {
336: //shows the current node
337: nodeBox = createNodeBox();
338: //shows those children of the current node which are not leaves
339: branchList = new JList();
340: //shows those children of the current node which are leaves
341: leafList = new JList();
342: //shows the current selection
343: entryField = new JTextField(30);
344: //label for branchList
345: branchLabel = new JLabel(DEFAULT_BRANCH_LABEL);
346: //label for leafList
347: leafLabel = new JLabel(DEFAULT_LEAF_LABEL);
348: //label for entryField
349: entryLabel = new JLabel(DEFAULT_ENTRY_LABEL);
350:
351: //wire them together
352: initListeners(nodeBox, branchList, leafList, entryLabel,
353: entryField);
354: initLayout(nodeBox, branchLabel, branchList, leafLabel,
355: leafList, entryLabel, entryField);
356: setListPath();
357: }
358:
359: private JComboBox createNodeBox() {
360: comboModel = new DefaultComboBoxModel();
361: setComboPath();
362: JComboBox combo = new JComboBox(comboModel);
363: combo.setRenderer(new ComboRenderer());
364: return combo;
365: }
366:
367: /**
368: * sets the comboBox model to show the current node and its ancestors.
369: * if the rootVisible property is false, all the root node's children, regardless
370: * of whether they are ancestors of the current node, are shown, as otherwise
371: * there is no way to navigate to them.
372: */
373: private void setComboPath() {
374: Debug.trace(this , Debug.DP5, "in setComboPath");
375: comboModel.removeAllElements();
376: Object[] pathObjs = selectedPath.getPath();
377: Debug.trace(this , Debug.DP5, "selectedPath: {0}", selectedPath);
378:
379: if (!isRootVisible()) {
380: Debug.trace(this , Debug.DP5, "root is not visible");
381: Object root = treeModel.getRoot();
382: for (int i = 0; i < treeModel.getChildCount(root); i++) {
383: Object subRoot = treeModel.getChild(root, i);
384: if (!treeModel.isLeaf(subRoot)) {
385: comboModel.addElement(subRoot);
386: Debug.trace(this , Debug.DP5, "adding subRoot "
387: + subRoot);
388: if (pathObjs.length > 2
389: && pathObjs[1].equals(subRoot)) {
390: Debug.trace(this , Debug.DP5,
391: "path contains subRoot!");
392: for (int j = 2; j < pathObjs.length; j++) {
393: comboModel.addElement(pathObjs[j]);
394: }
395: }
396: }
397: }
398: } else {
399: Debug.trace(this , Debug.DP5, "root is visible");
400: for (int i = 0; i < pathObjs.length; i++) {
401: comboModel.addElement(pathObjs[i]);
402: }
403: }
404: comboModel.setSelectedItem(pathObjs[pathObjs.length - 1]);
405: Debug.trace(this , Debug.DP5, "comboModel: {0}", comboModel);
406: }
407:
408: private void setListPath() {
409: Debug.trace(this , Debug.DP5, "in setListPath()");
410: Vector branchVector = new Vector();
411: Vector leafVector = new Vector();
412: TreeNode parent = (TreeNode) getSelectedPath()
413: .getLastPathComponent();
414: if (parent == null) {
415: Debug.trace(this , Debug.DP2, "selected path {0} is funky",
416: getSelectedPath());
417: return;
418: }
419: int childCnt = treeModel.getChildCount(parent);
420: for (int i = 0; i < childCnt; i++) {
421: TreeNode kid = (TreeNode) treeModel.getChild(parent, i);
422: if (kid.isLeaf()) {
423: leafVector.addElement(kid);
424: } else {
425: branchVector.addElement(kid);
426: }
427: }
428: branchList.setListData(branchVector);
429: leafList.setListData(leafVector);
430: }
431:
432: /**
433: * lays out the components.
434: * I'm emulating the GtkFileSelection widget,
435: * but at present leaving out certain features I don't need,
436: * like the buttons to create/delete/rename
437: */
438: private void initLayout(JComboBox nodeBox, JLabel branchLabel,
439: JList branchList, JLabel leafLabel, JList leafList,
440: JLabel entryLabel, JTextField entryField) {
441:
442: this .setLayout(new GridBagLayout());
443: GridBagConstraints gbc = new GridBagConstraints();
444:
445: gbc.gridx = 0;
446: gbc.gridy = 0;
447: gbc.gridheight = 1;
448: gbc.gridwidth = 2;
449: gbc.fill = GridBagConstraints.BOTH;
450: gbc.insets = new Insets(2, 2, 2, 2);
451: gbc.anchor = GridBagConstraints.CENTER;
452: this .add(nodeBox, gbc);
453:
454: gbc.gridy++;
455: gbc.gridwidth = 1;
456: gbc.anchor = GridBagConstraints.WEST;
457: this .add(branchLabel, gbc);
458:
459: gbc.gridx++;
460: this .add(leafLabel, gbc);
461:
462: gbc.gridx = 0;
463: gbc.gridy++;
464: Dimension d = new Dimension(180, 360);
465: JScrollPane tmpPane = new JScrollPane(branchList);
466: tmpPane.setPreferredSize(d);
467: this .add(tmpPane, gbc);
468:
469: gbc.gridx++;
470: tmpPane = new JScrollPane(leafList);
471: tmpPane.setPreferredSize(d);
472: this .add(tmpPane, gbc);
473:
474: gbc.gridx = 0;
475: gbc.gridy++;
476: gbc.gridwidth = 2;
477: this .add(entryLabel, gbc);
478:
479: gbc.gridy++;
480: this .add(entryField, gbc);
481: }
482:
483: private void initListeners(JComboBox nodeBox, JList branchList,
484: JList leafList, JLabel entryLabel, JTextField entryField) {
485: nodeBox.addActionListener(new ComboActionListener());
486: branchList
487: .addListSelectionListener(new BranchListSelectionListener());
488: branchList.addMouseListener(new BranchListMouseListener());
489: leafList
490: .addListSelectionListener(new LeafListSelectionListener());
491: }
492:
493: private Object[] getComboPath(Object value) {
494: Debug.trace(this , Debug.DP5, "in getComboPath{0}",
495: new Object[] { value });
496: if (isRootVisible()) {
497: int size = comboModel.getIndexOf(value) + 1;
498: Object[] path = new Object[size];
499: for (int i = 0; i < size; i++) {
500: path[i] = comboModel.getElementAt(i);
501: }
502: return path;
503: } else {
504: Debug.trace(this , Debug.DP5,
505: "in getComboPath, invisible root");
506: //a little more complicated, as the non-ancestor modes need to be eliminated.
507: Object selectedItem = comboModel.getSelectedItem();
508: int itemCount = comboModel.getSize();
509: //gather the tree's subroots
510: Object treeRoot = treeModel.getRoot();
511: int subRootCount = treeModel.getChildCount(treeRoot);
512: ArrayList subRoots = new ArrayList(subRootCount);
513: for (int i = 0; i < subRootCount; i++) {
514: subRoots.add(treeModel.getChild(treeRoot, i));
515: }
516: Debug.trace(this , Debug.DP5, "subRoots: {0}", subRoots);
517:
518: ArrayList pathList = new ArrayList();
519: //iterate through items in comboModel. if it is a subroot, set it
520: //as the first element in the path, eliminating whatever else is there.
521: //if equal to selected item, break. else
522: for (int i = 0; i < itemCount; i++) {
523: Object nextItem = comboModel.getElementAt(i);
524: if (subRoots.contains(nextItem)) {
525: //wipe out pathList if we have a subroot, in effect
526: //eliminating any previous subroot from the reported path
527: pathList.clear();
528: }
529: pathList.add(nextItem);
530: if (nextItem.equals(selectedItem))
531: break;
532: }
533: //insert root node
534: pathList.add(0, treeRoot);
535: Debug.trace(this , Debug.DP5,
536: "returning from getComboPath(): " + pathList);
537: return pathList.toArray();
538: }
539: }
540:
541: protected class ComboRenderer extends DefaultListCellRenderer {
542: protected ComboRenderer() {
543: super ();
544: }
545:
546: public Component getListCellRendererComponent(JList list,
547: Object value, int index, boolean isSelected,
548: boolean cellHasFocus) {
549: //from value we produce the corresponding treepath, then represent it
550: //in a path-like fashion.
551: return super .getListCellRendererComponent(list,
552: formatPath(getComboPath(value)), index, isSelected,
553: cellHasFocus);
554: }
555:
556: protected String formatPath(Object[] path) {
557: StringBuffer buffer = new StringBuffer();
558: for (int i = 0; i < path.length; i++) {
559: if (i == 0 && !isRootVisible())
560: continue;
561: buffer.append(path[i]).append(getNodeSeparator());
562: }
563: return buffer.toString();
564: }
565: }
566:
567: private class ComboActionListener implements ActionListener {
568: public void actionPerformed(ActionEvent ae) {
569: DefaultComboBoxModel model = TreeNodeChooser.this .comboModel;
570: Object selectedItem = model.getSelectedItem();
571: int index = model.getIndexOf(selectedItem);
572: if (index >= 0) {
573:
574: int offset = (isRootVisible()) ? 0 : 1;
575: int pathSize = index + 1 + offset;
576: Object[] path = new Object[pathSize];
577: synchronized (model) {
578: if (!isRootVisible())
579: path[0] = getModel().getRoot();
580: // note that I am looping over a collection
581: // from which I am also removing elements,
582: // normally not such a good thing.
583: for (int i = 0; i < model.getSize(); i++) {
584: if (i + offset < path.length) {
585: path[i + offset] = model.getElementAt(i);
586: } else {
587: model.removeElementAt(i);
588: }
589: }
590: }
591: setSelectedPath(new TreePath(path));
592: setListPath();
593: }
594: }
595: }
596:
597: private class BranchListSelectionListener implements
598: ListSelectionListener {
599: public void valueChanged(ListSelectionEvent lassie) {
600: if (!isEntryFieldTextSticky()) {
601: TreeNodeChooser.this .entryField.setText("");
602: if (!selectionMode.equals(SelectionMode.LEAF_ONLY)) {
603: Object branchValue = TreeNodeChooser.this .branchList
604: .getSelectedValue();
605: if (branchValue != null)
606: TreeNodeChooser.this .entryField
607: .setText(branchValue.toString());
608: }
609: }
610: }
611: }
612:
613: private class BranchListMouseListener extends MouseAdapter {
614: public void mouseClicked(MouseEvent eek) {
615: if (eek.getClickCount() == 2) {
616: JList daList = TreeNodeChooser.this .branchList;
617: int index = daList.locationToIndex(eek.getPoint());
618: if (index < 0) {
619: Debug.trace(this , Debug.DP5,
620: "JList mouse event reports negative index: "
621: + index);
622: return;
623: }
624: Object branch = daList.getModel().getElementAt(index);
625: setCurrentPath(getSelectedPath().pathByAddingChild(
626: branch));
627: }
628: }
629: }
630:
631: private class LeafListSelectionListener implements
632: ListSelectionListener {
633: public void valueChanged(ListSelectionEvent lassie) {
634: if (!selectionMode.equals(SelectionMode.BRANCH_ONLY)) {
635: Object leafValue = TreeNodeChooser.this .leafList
636: .getSelectedValue();
637: if (leafValue != null)
638: TreeNodeChooser.this .entryField.setText(leafValue
639: .toString());
640: }
641: }
642: }
643:
644: /**
645: * an enumerated type to represent chooser selection modes
646: */
647: public static final class SelectionMode {
648: /**
649: * Only leaves can be selected
650: */
651: public static final SelectionMode LEAF_ONLY = new SelectionMode(
652: "leaf_only");
653:
654: /**
655: * Both leaves and branches can be selected
656: */
657: public static final SelectionMode LEAF_AND_BRANCH = new SelectionMode(
658: "leaf_and_branch");
659:
660: /**
661: * Only branches can be selected
662: */
663: public static final SelectionMode BRANCH_ONLY = new SelectionMode(
664: "branch_only");
665:
666: private String s;
667:
668: private SelectionMode(String s) {
669: this .s = s;
670: }
671:
672: public String toString() {
673: return s;
674: }
675: }
676: }
677:
678: /* $Log: TreeNodeChooser.java,v $
679: /* Revision 1.4 2001/01/04 06:02:49 smulloni
680: /* added more javadoc documentation.
681: /*
682: /* Revision 1.3 2001/01/03 20:11:31 smulloni
683: /* the DAVFileChooser now replaces JFileChooser for remote file access.
684: /* DAVMethod now has a protocol property.
685: /*
686: /* Revision 1.2 2001/01/03 00:30:35 smulloni
687: /* a number of modifications along the way to replacing JFileChooser with
688: /* something more suitable for remote (virtual) files.
689: /*
690: /* Revision 1.1 2001/01/02 15:53:51 smulloni
691: /* draft of filechooser
692: /* */
|