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}