001/*
002 * $Id: TreePanel.java,v 1.6 2011/03/24 16:06:35 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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 */
030package edu.wisc.ssec.mcidasv.util;
031
032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newMap;
033import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
034
035import java.awt.BorderLayout;
036import java.awt.Component;
037import java.awt.Dimension;
038import java.util.Enumeration;
039import java.util.Hashtable;
040import java.util.List;
041import java.util.Map;
042import java.util.StringTokenizer;
043
044import javax.swing.ImageIcon;
045import javax.swing.JComponent;
046import javax.swing.JPanel;
047import javax.swing.JScrollPane;
048import javax.swing.JSplitPane;
049import javax.swing.JTree;
050import javax.swing.event.TreeSelectionEvent;
051import javax.swing.event.TreeSelectionListener;
052import javax.swing.tree.DefaultMutableTreeNode;
053import javax.swing.tree.DefaultTreeCellRenderer;
054import javax.swing.tree.DefaultTreeModel;
055import javax.swing.tree.TreeNode;
056import javax.swing.tree.TreePath;
057
058import ucar.unidata.util.GuiUtils;
059import ucar.unidata.util.StringUtil;
060import ucar.unidata.util.TwoFacedObject;
061
062import 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") 
070public 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}