001    /*
002     * $Id: TreePanel.java,v 1.7 2012/02/19 17:35:52 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    package edu.wisc.ssec.mcidasv.util;
031    
032    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newMap;
033    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
034    
035    import java.awt.BorderLayout;
036    import java.awt.Component;
037    import java.awt.Dimension;
038    import java.util.Enumeration;
039    import java.util.Hashtable;
040    import java.util.List;
041    import java.util.Map;
042    import java.util.StringTokenizer;
043    
044    import javax.swing.ImageIcon;
045    import javax.swing.JComponent;
046    import javax.swing.JPanel;
047    import javax.swing.JScrollPane;
048    import javax.swing.JSplitPane;
049    import javax.swing.JTree;
050    import javax.swing.event.TreeSelectionEvent;
051    import javax.swing.event.TreeSelectionListener;
052    import javax.swing.tree.DefaultMutableTreeNode;
053    import javax.swing.tree.DefaultTreeCellRenderer;
054    import javax.swing.tree.DefaultTreeModel;
055    import javax.swing.tree.TreeNode;
056    import javax.swing.tree.TreePath;
057    
058    import ucar.unidata.util.GuiUtils;
059    import ucar.unidata.util.StringUtil;
060    import ucar.unidata.util.TwoFacedObject;
061    
062    import edu.wisc.ssec.mcidasv.McIDASV;
063    
064    /**
065     *  This class shows a tree on the left and a card panel on the right.
066     *  
067     *  Ripped right out of the IDV, for the time being.
068     */
069    @SuppressWarnings("serial") 
070    public class TreePanel extends JPanel implements TreeSelectionListener {
071    
072        public static final String CATEGORY_DELIMITER = ">";
073    
074        /** The root */
075        private final DefaultMutableTreeNode root = new DefaultMutableTreeNode("");
076    
077        /** the model */
078        private final DefaultTreeModel treeModel = new DefaultTreeModel(root);
079    
080        /** the tree */
081        private final JTree tree = new JTree(treeModel);
082    
083        /** The scroller */
084        private final JScrollPane treeView = new JScrollPane(tree);
085    
086        /** The panel */
087        private GuiUtils.CardLayoutPanel panel;
088    
089        /** _more_ */
090        private final JPanel emptyPanel = new JPanel(new BorderLayout());
091    
092        /** _more_ */
093        private final Map<String, Component> catComponents = newMap();
094    
095        /** Maps categories to tree node */
096        private final Map<String, DefaultMutableTreeNode> catToNode = newMap();
097    
098        /** Maps components to tree node */
099        private final Map<Component, DefaultMutableTreeNode> compToNode = newMap();
100    
101        /** ok to respond to selection changes */
102        private boolean okToUpdateTree = true;
103    
104        private boolean okToSave = false;
105    
106        /**
107         * ctor
108         */
109        public TreePanel() {
110            this(true, -1);
111        }
112    
113        /**
114         * _more_
115         *
116         * @param useSplitPane _more_
117         * @param treeWidth _more_
118         */
119        public TreePanel(boolean useSplitPane, int treeWidth) {
120            setLayout(new BorderLayout());
121            tree.setRootVisible(false);
122            tree.setShowsRootHandles(true);
123    
124            DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer() {
125                public Component getTreeCellRendererComponent(JTree theTree,
126                    Object value, boolean sel, boolean expanded, boolean leaf, 
127                    int row, boolean hasFocus) 
128                {
129                    super.getTreeCellRendererComponent(theTree, value, sel,
130                        expanded, leaf, row, hasFocus);
131    
132                    if (!(value instanceof MyTreeNode))
133                        return this;
134    
135                    MyTreeNode node = (MyTreeNode) value;
136                    if (node.icon != null)
137                        setIcon(node.icon);
138                    else
139                        setIcon(null);
140    
141                    return this;
142                }
143            };
144            renderer.setIcon(null);
145            renderer.setOpenIcon(null);
146            renderer.setClosedIcon(null);
147            tree.setCellRenderer(renderer);
148    
149            panel = new GuiUtils.CardLayoutPanel() {
150                public void show(Component comp) {
151                    super.show(comp);
152                    showPath(panel.getVisibleComponent());
153                }
154            };
155            panel.addCard(emptyPanel);
156    
157            if (treeWidth > 0)
158                treeView.setPreferredSize(new Dimension(treeWidth, 100));
159    
160            JComponent center;
161            if (useSplitPane) {
162                JSplitPane splitPane = ((treeWidth > 0)
163                                        ? GuiUtils.hsplit(treeView, panel, treeWidth)
164                                        : GuiUtils.hsplit(treeView, panel, 150));
165                center = splitPane;
166                splitPane.setOneTouchExpandable(true);
167            } else {
168                center = GuiUtils.leftCenter(treeView, panel);
169            }
170    
171            this.add(BorderLayout.CENTER, center);
172            tree.addTreeSelectionListener(this);
173        }
174    
175        public Component getVisibleComponent() {
176            return panel.getVisibleComponent();
177        }
178    
179        /**
180         * Handle tree selection changed
181         *
182         * @param e event
183         */
184        public void valueChanged(TreeSelectionEvent e) {
185            if (!okToUpdateTree)
186                return;
187    
188            DefaultMutableTreeNode node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
189            if (node == null)
190                return;
191    
192            saveCurrentPath(node);
193    
194            if (node.isLeaf()) {
195                TwoFacedObject tfo = (TwoFacedObject)node.getUserObject();
196                panel.show((Component) tfo.getId());
197            } else {
198                if (node.getUserObject() instanceof TwoFacedObject) {
199                    TwoFacedObject tfo = (TwoFacedObject)node.getUserObject();
200                    JComponent interior = (JComponent)catComponents.get(tfo.getId());
201                    if (interior == null)
202                        return;
203    
204                    if (!panel.contains(interior))
205                        panel.addCard(interior);
206    
207                    panel.show(interior);
208                    return;
209                }
210                panel.show(emptyPanel);
211            }
212        }
213    
214        public void setIcon(Component comp, ImageIcon icon) {
215            MyTreeNode node = (MyTreeNode)compToNode.get(comp);
216            if (node != null) {
217                node.icon = icon;
218                tree.repaint();
219            }
220        }
221    
222        /**
223         * Add the component to the panel
224         *
225         * @param component component
226         * @param category tree category. May be null.
227         * @param label Tree node label
228         * @param icon Node icon. May be null.
229         */
230        public void addComponent(JComponent component, String category, 
231            String label, ImageIcon icon) 
232        {
233            TwoFacedObject tfo = new TwoFacedObject(label, component);
234            DefaultMutableTreeNode panelNode = new MyTreeNode(tfo, icon);
235            compToNode.put(component, panelNode);
236    
237            if (category == null) {
238                root.add(panelNode);
239            } else {
240                List<String> toks = StringUtil.split(category, CATEGORY_DELIMITER, true, true);
241                String catSoFar = "";
242                DefaultMutableTreeNode catNode  = root;
243                for (int i = 0; i < toks.size(); i++) {
244                    String cat = toks.get(i);
245                    catSoFar = catSoFar + CATEGORY_DELIMITER + cat;
246                    DefaultMutableTreeNode node = catToNode.get(catSoFar);
247                    if (node == null) {
248                        TwoFacedObject catTfo = new TwoFacedObject(cat, catSoFar);
249                        node = new DefaultMutableTreeNode(catTfo);
250                        catToNode.put(catSoFar, node);
251                        catNode.add(node);
252                    }
253                    catNode = node;
254                }
255                catNode.add(panelNode);
256            }
257            panel.addCard(component);
258            treeChanged();
259        }
260    
261        private void treeChanged() {
262            // presumably okay--this method is older IDV code.
263            @SuppressWarnings("unchecked")
264            Hashtable stuff = GuiUtils.initializeExpandedPathsBeforeChange(tree, root);
265            treeModel.nodeStructureChanged(root);
266            GuiUtils.expandPathsAfterChange(tree, stuff, root);
267        }
268    
269        /**
270         * _more_
271         *
272         * @param cat _more_
273         * @param comp _more_
274         */
275        public void addCategoryComponent(String cat, JComponent comp) {
276            catComponents.put(CATEGORY_DELIMITER + cat, comp);
277        }
278    
279        /**
280         * _more_
281         *
282         * @param component _more_
283         */
284        public void removeComponent(JComponent component) {
285            DefaultMutableTreeNode node = compToNode.get(component);
286            if (node == null) {
287                return;
288            }
289            compToNode.remove(component);
290            if (node.getParent() != null) {
291                node.removeFromParent();
292            }
293            panel.remove(component);
294            treeChanged();
295        }
296    
297        public void show(Component component) {
298            panel.show(component);
299        }
300    
301        /**
302         * Show the tree node that corresponds to the component
303         *
304         * @param component comp
305         */
306        public void showPath(Component component) {
307            if (component != null) {
308                DefaultMutableTreeNode node = compToNode.get(component);
309                if (node != null) {
310                    TreePath path = new TreePath(treeModel.getPathToRoot(node));
311                    okToUpdateTree = false;
312                    tree.setSelectionPath(path);
313                    tree.expandPath(path);
314                    okToUpdateTree = true;
315                }
316            }
317        }
318    
319        /**
320         * Open all paths
321         */
322        public void openAll() {
323            for (int i = 0; i < tree.getRowCount(); i++)
324                tree.expandPath(tree.getPathForRow(i));
325            showPath(panel.getVisibleComponent());
326        }
327    
328        /**
329         * Close all paths
330         */
331        public void closeAll() {
332            for (int i = 0; i < tree.getRowCount(); i++)
333                tree.collapsePath(tree.getPathForRow(i));
334            showPath(panel.getVisibleComponent());
335        }
336    
337        /**
338         * Attempts to select the path from a previous McIDAS-V session. If no 
339         * path was persisted, the method attempts to use the {@literal "first"} 
340         * non-leaf node. 
341         * 
342         * <p>This method also sets {@link #okToSave} to {@code true}, so that 
343         * user selections can be captured after this method quits.
344         */
345        public void showPersistedSelection() {
346            okToSave = true;
347    
348            String path = loadSavedPath();
349    
350            TreePath tp = findByName(tree, tokenizePath(path));
351            if (tp == null || tp.getPathCount() == 1)
352                tp = getPathToFirstLeaf(new TreePath(root));
353    
354            tree.setSelectionPath(tp);
355            tree.expandPath(tp);
356        }
357    
358        private void saveCurrentPath(final DefaultMutableTreeNode node) {
359            assert node != null;
360            if (!okToSave)
361                return;
362    
363            McIDASV mcv = McIDASV.getStaticMcv();
364            if (mcv != null)
365                mcv.getStore().put("mcv.treepanel.savedpath", getPath(node));
366        }
367    
368        private String loadSavedPath() {
369            String path = "";
370            McIDASV mcv = McIDASV.getStaticMcv();
371            if (mcv == null)
372                return path;
373    
374            path = mcv.getStore().get("mcv.treepanel.savedpath", "");
375            if (path.length() > 0)
376                return path;
377    
378            TreePath tp = getPathToFirstLeaf(new TreePath(root));
379            DefaultMutableTreeNode node = (DefaultMutableTreeNode)tp.getLastPathComponent();
380            path = TreePanel.getPath(node);
381            mcv.getStore().put("mcv.treepanel.savedpath", path);
382    
383            return path;
384        }
385    
386        public static List<String> tokenizePath(final String path) {
387            if (path == null)
388                throw new NullPointerException("Cannot tokenize a null path");
389    
390            List<String> tokens = arrList();
391            StringTokenizer tokenizer = new StringTokenizer(path, CATEGORY_DELIMITER);
392            tokens.add("");
393            while (tokenizer.hasMoreTokens()) {
394                tokens.add(tokenizer.nextToken());
395            }
396            return tokens;
397        }
398    
399        public static String getPath(final DefaultMutableTreeNode node) {
400            if (node == null)
401                throw new NullPointerException("Cannot get the path of a null node");
402    
403            StringBuilder path = new StringBuilder("");
404            TreeNode[] nodes = node.getPath();
405            TreeNode root = nodes[0];
406            for (TreeNode n : nodes) {
407                if (n == root)
408                    path.append(n.toString());
409                else
410                    path.append(CATEGORY_DELIMITER + n.toString());
411            }
412            return path.toString();
413        }
414    
415        public static DefaultMutableTreeNode findNodeByPath(JTree tree, String path) {
416            TreePath tpath = findByName(tree, tokenizePath(path));
417            if (tpath == null)
418                return null;
419    
420            return (DefaultMutableTreeNode)tpath.getLastPathComponent();
421        }
422    
423        public static TreePath findByName(JTree tree, List<String> names) {
424            TreeNode root = (TreeNode)tree.getModel().getRoot();
425            return searchTree(new TreePath(root), names, 0);
426        }
427    
428        @SuppressWarnings("unchecked") 
429        private static TreePath searchTree(TreePath parent, List<String> nodes, int depth) {
430            assert parent != null;
431            assert nodes != null;
432            assert depth >= 0;
433    
434            TreeNode node = (TreeNode)parent.getLastPathComponent();
435            if (node == null)
436                return null;
437    
438            String payload = node.toString();
439    
440            // If equal, go down the branch
441            if (nodes.get(depth) == null)
442                return null;
443    
444            if (payload.equals(nodes.get(depth).toString())) {
445                // If at end, return match
446                if (depth == nodes.size() - 1)
447                    return parent;
448    
449                // Traverse children
450                if (node.getChildCount() >= 0) {
451                    for (Enumeration<TreeNode> e = node.children(); e.hasMoreElements();) {
452                        TreeNode n = e.nextElement();
453                        TreePath path = parent.pathByAddingChild(n);
454                        TreePath result = searchTree(path, nodes, depth + 1);
455    
456                        // Found a match
457                        if (result != null)
458                            return result;
459                    }
460                }
461            }
462    
463            // No match at this branch
464            return null;
465        }
466    
467        @SuppressWarnings("unchecked") 
468        private static TreePath getPathToFirstLeaf(final TreePath searchPath) {
469            TreeNode node = (TreeNode)searchPath.getLastPathComponent();
470            if (node == null)
471                return null;
472    
473            if (node.isLeaf())
474                return searchPath;
475    
476            for (Enumeration<TreeNode> e = node.children(); e.hasMoreElements();) {
477                TreeNode n = e.nextElement();
478                TreePath newPath = searchPath.pathByAddingChild(n);
479                TreePath result = getPathToFirstLeaf(newPath);
480                if (result != null)
481                    return result;
482            }
483            return null;
484        }
485    
486        /**
487         * TreeNode extensions that allows us to associate an icon with this node.
488         */
489        private static class MyTreeNode extends DefaultMutableTreeNode {
490            public ImageIcon icon;
491    
492            public MyTreeNode(Object o, ImageIcon icon) {
493                super(o);
494                this.icon = icon;
495            }
496        }
497    }