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