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