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 }