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 }