001 /* 002 * $Id: TreePanel.java,v 1.7 2012/02/19 17:35:52 davep Exp $ 003 * 004 * This file is part of McIDAS-V 005 * 006 * Copyright 2007-2012 007 * Space Science and Engineering Center (SSEC) 008 * University of Wisconsin - Madison 009 * 1225 W. Dayton Street, Madison, WI 53706, USA 010 * https://www.ssec.wisc.edu/mcidas 011 * 012 * All Rights Reserved 013 * 014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 015 * some McIDAS-V source code is based on IDV and VisAD source code. 016 * 017 * McIDAS-V is free software; you can redistribute it and/or modify 018 * it under the terms of the GNU Lesser Public License as published by 019 * the Free Software Foundation; either version 3 of the License, or 020 * (at your option) any later version. 021 * 022 * McIDAS-V is distributed in the hope that it will be useful, 023 * but WITHOUT ANY WARRANTY; without even the implied warranty of 024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 025 * GNU Lesser Public License for more details. 026 * 027 * You should have received a copy of the GNU Lesser Public License 028 * along with this program. If not, see http://www.gnu.org/licenses. 029 */ 030 package edu.wisc.ssec.mcidasv.util; 031 032 import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newMap; 033 import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList; 034 035 import java.awt.BorderLayout; 036 import java.awt.Component; 037 import java.awt.Dimension; 038 import java.util.Enumeration; 039 import java.util.Hashtable; 040 import java.util.List; 041 import java.util.Map; 042 import java.util.StringTokenizer; 043 044 import javax.swing.ImageIcon; 045 import javax.swing.JComponent; 046 import javax.swing.JPanel; 047 import javax.swing.JScrollPane; 048 import javax.swing.JSplitPane; 049 import javax.swing.JTree; 050 import javax.swing.event.TreeSelectionEvent; 051 import javax.swing.event.TreeSelectionListener; 052 import javax.swing.tree.DefaultMutableTreeNode; 053 import javax.swing.tree.DefaultTreeCellRenderer; 054 import javax.swing.tree.DefaultTreeModel; 055 import javax.swing.tree.TreeNode; 056 import javax.swing.tree.TreePath; 057 058 import ucar.unidata.util.GuiUtils; 059 import ucar.unidata.util.StringUtil; 060 import ucar.unidata.util.TwoFacedObject; 061 062 import 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 * Ripped right out of the IDV, for the time being. 068 */ 069 @SuppressWarnings("serial") 070 public class TreePanel extends JPanel implements TreeSelectionListener { 071 072 public static final String CATEGORY_DELIMITER = ">"; 073 074 /** The root */ 075 private final DefaultMutableTreeNode root = new DefaultMutableTreeNode(""); 076 077 /** the model */ 078 private final DefaultTreeModel treeModel = new DefaultTreeModel(root); 079 080 /** the tree */ 081 private final JTree tree = new JTree(treeModel); 082 083 /** The scroller */ 084 private final JScrollPane treeView = new JScrollPane(tree); 085 086 /** The panel */ 087 private GuiUtils.CardLayoutPanel panel; 088 089 /** _more_ */ 090 private final JPanel emptyPanel = new JPanel(new BorderLayout()); 091 092 /** _more_ */ 093 private final Map<String, Component> catComponents = newMap(); 094 095 /** Maps categories to tree node */ 096 private final Map<String, DefaultMutableTreeNode> catToNode = newMap(); 097 098 /** Maps components to tree node */ 099 private final Map<Component, DefaultMutableTreeNode> compToNode = newMap(); 100 101 /** ok to respond to selection changes */ 102 private boolean okToUpdateTree = true; 103 104 private boolean okToSave = false; 105 106 /** 107 * ctor 108 */ 109 public TreePanel() { 110 this(true, -1); 111 } 112 113 /** 114 * _more_ 115 * 116 * @param useSplitPane _more_ 117 * @param treeWidth _more_ 118 */ 119 public TreePanel(boolean useSplitPane, int treeWidth) { 120 setLayout(new BorderLayout()); 121 tree.setRootVisible(false); 122 tree.setShowsRootHandles(true); 123 124 DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer() { 125 public Component getTreeCellRendererComponent(JTree theTree, 126 Object value, boolean sel, boolean expanded, boolean leaf, 127 int row, boolean hasFocus) 128 { 129 super.getTreeCellRendererComponent(theTree, value, sel, 130 expanded, leaf, row, hasFocus); 131 132 if (!(value instanceof MyTreeNode)) 133 return this; 134 135 MyTreeNode node = (MyTreeNode) value; 136 if (node.icon != null) 137 setIcon(node.icon); 138 else 139 setIcon(null); 140 141 return this; 142 } 143 }; 144 renderer.setIcon(null); 145 renderer.setOpenIcon(null); 146 renderer.setClosedIcon(null); 147 tree.setCellRenderer(renderer); 148 149 panel = new GuiUtils.CardLayoutPanel() { 150 public void show(Component comp) { 151 super.show(comp); 152 showPath(panel.getVisibleComponent()); 153 } 154 }; 155 panel.addCard(emptyPanel); 156 157 if (treeWidth > 0) 158 treeView.setPreferredSize(new Dimension(treeWidth, 100)); 159 160 JComponent center; 161 if (useSplitPane) { 162 JSplitPane splitPane = ((treeWidth > 0) 163 ? GuiUtils.hsplit(treeView, panel, treeWidth) 164 : GuiUtils.hsplit(treeView, panel, 150)); 165 center = splitPane; 166 splitPane.setOneTouchExpandable(true); 167 } else { 168 center = GuiUtils.leftCenter(treeView, panel); 169 } 170 171 this.add(BorderLayout.CENTER, center); 172 tree.addTreeSelectionListener(this); 173 } 174 175 public Component getVisibleComponent() { 176 return panel.getVisibleComponent(); 177 } 178 179 /** 180 * Handle tree selection changed 181 * 182 * @param e event 183 */ 184 public void valueChanged(TreeSelectionEvent e) { 185 if (!okToUpdateTree) 186 return; 187 188 DefaultMutableTreeNode node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); 189 if (node == null) 190 return; 191 192 saveCurrentPath(node); 193 194 if (node.isLeaf()) { 195 TwoFacedObject tfo = (TwoFacedObject)node.getUserObject(); 196 panel.show((Component) tfo.getId()); 197 } else { 198 if (node.getUserObject() instanceof TwoFacedObject) { 199 TwoFacedObject tfo = (TwoFacedObject)node.getUserObject(); 200 JComponent interior = (JComponent)catComponents.get(tfo.getId()); 201 if (interior == null) 202 return; 203 204 if (!panel.contains(interior)) 205 panel.addCard(interior); 206 207 panel.show(interior); 208 return; 209 } 210 panel.show(emptyPanel); 211 } 212 } 213 214 public void setIcon(Component comp, ImageIcon icon) { 215 MyTreeNode node = (MyTreeNode)compToNode.get(comp); 216 if (node != null) { 217 node.icon = icon; 218 tree.repaint(); 219 } 220 } 221 222 /** 223 * Add the component to the panel 224 * 225 * @param component component 226 * @param category tree category. May be null. 227 * @param label Tree node label 228 * @param icon Node icon. May be null. 229 */ 230 public void addComponent(JComponent component, String category, 231 String label, ImageIcon icon) 232 { 233 TwoFacedObject tfo = new TwoFacedObject(label, component); 234 DefaultMutableTreeNode panelNode = new MyTreeNode(tfo, icon); 235 compToNode.put(component, panelNode); 236 237 if (category == null) { 238 root.add(panelNode); 239 } else { 240 List<String> toks = StringUtil.split(category, CATEGORY_DELIMITER, true, true); 241 String catSoFar = ""; 242 DefaultMutableTreeNode catNode = root; 243 for (int i = 0; i < toks.size(); i++) { 244 String cat = toks.get(i); 245 catSoFar = catSoFar + CATEGORY_DELIMITER + cat; 246 DefaultMutableTreeNode node = catToNode.get(catSoFar); 247 if (node == null) { 248 TwoFacedObject catTfo = new TwoFacedObject(cat, catSoFar); 249 node = new DefaultMutableTreeNode(catTfo); 250 catToNode.put(catSoFar, node); 251 catNode.add(node); 252 } 253 catNode = node; 254 } 255 catNode.add(panelNode); 256 } 257 panel.addCard(component); 258 treeChanged(); 259 } 260 261 private void treeChanged() { 262 // presumably okay--this method is older IDV code. 263 @SuppressWarnings("unchecked") 264 Hashtable stuff = GuiUtils.initializeExpandedPathsBeforeChange(tree, root); 265 treeModel.nodeStructureChanged(root); 266 GuiUtils.expandPathsAfterChange(tree, stuff, root); 267 } 268 269 /** 270 * _more_ 271 * 272 * @param cat _more_ 273 * @param comp _more_ 274 */ 275 public void addCategoryComponent(String cat, JComponent comp) { 276 catComponents.put(CATEGORY_DELIMITER + cat, comp); 277 } 278 279 /** 280 * _more_ 281 * 282 * @param component _more_ 283 */ 284 public void removeComponent(JComponent component) { 285 DefaultMutableTreeNode node = compToNode.get(component); 286 if (node == null) { 287 return; 288 } 289 compToNode.remove(component); 290 if (node.getParent() != null) { 291 node.removeFromParent(); 292 } 293 panel.remove(component); 294 treeChanged(); 295 } 296 297 public void show(Component component) { 298 panel.show(component); 299 } 300 301 /** 302 * Show the tree node that corresponds to the component 303 * 304 * @param component comp 305 */ 306 public void showPath(Component component) { 307 if (component != null) { 308 DefaultMutableTreeNode node = compToNode.get(component); 309 if (node != null) { 310 TreePath path = new TreePath(treeModel.getPathToRoot(node)); 311 okToUpdateTree = false; 312 tree.setSelectionPath(path); 313 tree.expandPath(path); 314 okToUpdateTree = true; 315 } 316 } 317 } 318 319 /** 320 * Open all paths 321 */ 322 public void openAll() { 323 for (int i = 0; i < tree.getRowCount(); i++) 324 tree.expandPath(tree.getPathForRow(i)); 325 showPath(panel.getVisibleComponent()); 326 } 327 328 /** 329 * Close all paths 330 */ 331 public void closeAll() { 332 for (int i = 0; i < tree.getRowCount(); i++) 333 tree.collapsePath(tree.getPathForRow(i)); 334 showPath(panel.getVisibleComponent()); 335 } 336 337 /** 338 * Attempts to select the path from a previous McIDAS-V session. If no 339 * path was persisted, the method attempts to use the {@literal "first"} 340 * non-leaf node. 341 * 342 * <p>This method also sets {@link #okToSave} to {@code true}, so that 343 * user selections can be captured after this method quits. 344 */ 345 public void showPersistedSelection() { 346 okToSave = true; 347 348 String path = loadSavedPath(); 349 350 TreePath tp = findByName(tree, tokenizePath(path)); 351 if (tp == null || tp.getPathCount() == 1) 352 tp = getPathToFirstLeaf(new TreePath(root)); 353 354 tree.setSelectionPath(tp); 355 tree.expandPath(tp); 356 } 357 358 private void saveCurrentPath(final DefaultMutableTreeNode node) { 359 assert node != null; 360 if (!okToSave) 361 return; 362 363 McIDASV mcv = McIDASV.getStaticMcv(); 364 if (mcv != null) 365 mcv.getStore().put("mcv.treepanel.savedpath", getPath(node)); 366 } 367 368 private String loadSavedPath() { 369 String path = ""; 370 McIDASV mcv = McIDASV.getStaticMcv(); 371 if (mcv == null) 372 return path; 373 374 path = mcv.getStore().get("mcv.treepanel.savedpath", ""); 375 if (path.length() > 0) 376 return path; 377 378 TreePath tp = getPathToFirstLeaf(new TreePath(root)); 379 DefaultMutableTreeNode node = (DefaultMutableTreeNode)tp.getLastPathComponent(); 380 path = TreePanel.getPath(node); 381 mcv.getStore().put("mcv.treepanel.savedpath", path); 382 383 return path; 384 } 385 386 public static List<String> tokenizePath(final String path) { 387 if (path == null) 388 throw new NullPointerException("Cannot tokenize a null path"); 389 390 List<String> tokens = arrList(); 391 StringTokenizer tokenizer = new StringTokenizer(path, CATEGORY_DELIMITER); 392 tokens.add(""); 393 while (tokenizer.hasMoreTokens()) { 394 tokens.add(tokenizer.nextToken()); 395 } 396 return tokens; 397 } 398 399 public static String getPath(final DefaultMutableTreeNode node) { 400 if (node == null) 401 throw new NullPointerException("Cannot get the path of a null node"); 402 403 StringBuilder path = new StringBuilder(""); 404 TreeNode[] nodes = node.getPath(); 405 TreeNode root = nodes[0]; 406 for (TreeNode n : nodes) { 407 if (n == root) 408 path.append(n.toString()); 409 else 410 path.append(CATEGORY_DELIMITER + n.toString()); 411 } 412 return path.toString(); 413 } 414 415 public static DefaultMutableTreeNode findNodeByPath(JTree tree, String path) { 416 TreePath tpath = findByName(tree, tokenizePath(path)); 417 if (tpath == null) 418 return null; 419 420 return (DefaultMutableTreeNode)tpath.getLastPathComponent(); 421 } 422 423 public static TreePath findByName(JTree tree, List<String> names) { 424 TreeNode root = (TreeNode)tree.getModel().getRoot(); 425 return searchTree(new TreePath(root), names, 0); 426 } 427 428 @SuppressWarnings("unchecked") 429 private static TreePath searchTree(TreePath parent, List<String> nodes, int depth) { 430 assert parent != null; 431 assert nodes != null; 432 assert depth >= 0; 433 434 TreeNode node = (TreeNode)parent.getLastPathComponent(); 435 if (node == null) 436 return null; 437 438 String payload = node.toString(); 439 440 // If equal, go down the branch 441 if (nodes.get(depth) == null) 442 return null; 443 444 if (payload.equals(nodes.get(depth).toString())) { 445 // If at end, return match 446 if (depth == nodes.size() - 1) 447 return parent; 448 449 // Traverse children 450 if (node.getChildCount() >= 0) { 451 for (Enumeration<TreeNode> e = node.children(); e.hasMoreElements();) { 452 TreeNode n = e.nextElement(); 453 TreePath path = parent.pathByAddingChild(n); 454 TreePath result = searchTree(path, nodes, depth + 1); 455 456 // Found a match 457 if (result != null) 458 return result; 459 } 460 } 461 } 462 463 // No match at this branch 464 return null; 465 } 466 467 @SuppressWarnings("unchecked") 468 private static TreePath getPathToFirstLeaf(final TreePath searchPath) { 469 TreeNode node = (TreeNode)searchPath.getLastPathComponent(); 470 if (node == null) 471 return null; 472 473 if (node.isLeaf()) 474 return searchPath; 475 476 for (Enumeration<TreeNode> e = node.children(); e.hasMoreElements();) { 477 TreeNode n = e.nextElement(); 478 TreePath newPath = searchPath.pathByAddingChild(n); 479 TreePath result = getPathToFirstLeaf(newPath); 480 if (result != null) 481 return result; 482 } 483 return null; 484 } 485 486 /** 487 * TreeNode extensions that allows us to associate an icon with this node. 488 */ 489 private static class MyTreeNode extends DefaultMutableTreeNode { 490 public ImageIcon icon; 491 492 public MyTreeNode(Object o, ImageIcon icon) { 493 super(o); 494 this.icon = icon; 495 } 496 } 497 }