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 }