001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 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}