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