001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 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 https://www.gnu.org/licenses/. 027 */ 028 029package edu.wisc.ssec.mcidasv.startupmanager.options; 030 031import java.awt.BorderLayout; 032import java.awt.Dimension; 033import java.io.File; 034import java.util.regex.Pattern; 035 036import javax.swing.JCheckBox; 037import javax.swing.JPanel; 038import javax.swing.JScrollPane; 039import javax.swing.JTree; 040import javax.swing.ToolTipManager; 041import javax.swing.event.AncestorEvent; 042import javax.swing.event.AncestorListener; 043import javax.swing.tree.DefaultMutableTreeNode; 044import javax.swing.tree.DefaultTreeModel; 045import javax.swing.tree.TreePath; 046import javax.swing.tree.TreeSelectionModel; 047 048import edu.wisc.ssec.mcidasv.ArgumentManager; 049import edu.wisc.ssec.mcidasv.startupmanager.StartupManager; 050import edu.wisc.ssec.mcidasv.startupmanager.StartupManager.TreeCellRenderer; 051import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.OptionPlatform; 052import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.Type; 053import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.Visibility; 054import edu.wisc.ssec.mcidasv.util.MakeToString; 055 056/** 057 * Represents a startup option that should be selected from the contents of a 058 * given directory. The visual representation of this class is a tree. 059 */ 060 061public final class DirectoryOption extends AbstractOption { 062 063 /** Regular expression pattern for ensuring that no quote marks are present in {@link #value}. */ 064 private static final Pattern CLEAN_VALUE_REGEX = Pattern.compile("\""); 065 066 /** Selected tree node. Value may be {@code null}. */ 067 private DefaultMutableTreeNode selected = null; 068 069 /** Current option value. Empty {@code String} signifies no selection. */ 070 private String value = ""; 071 072 /** Default value of this option. */ 073 private final String defaultValue; 074 075 public DirectoryOption(final String id, final String label, final String defaultValue, final OptionPlatform optionPlatform, final Visibility optionVisibility) { 076 super(id, label, Type.DIRTREE, optionPlatform, optionVisibility); 077 this.defaultValue = defaultValue; 078 setValue(defaultValue); 079 } 080 081 private void exploreDirectory(final String directory, final DefaultMutableTreeNode parent) { 082 assert directory != null : "Cannot traverse a null directory"; 083 System.err.println("scanning bundle directory: '"+directory+'\''); 084 File dir = new File(directory); 085 assert dir.exists() : "Cannot traverse a directory that does not exist"; 086 087 File[] files = dir.listFiles(); 088 assert files != null; 089 for (File f : files) { 090 DefaultMutableTreeNode current = new DefaultMutableTreeNode(f); 091 if (f.isDirectory()) { 092 System.err.println(f+": directory!"); 093 parent.add(current); 094 exploreDirectory(f.getPath(), current); 095 } else if (ArgumentManager.isBundle(f.getPath())) { 096 System.err.println(f+": bundle!"); 097 parent.add(current); 098 if (f.getPath().equals(getUnquotedValue())) { 099 selected = current; 100 } 101 } else { 102 System.err.println(f+": neither! :("); 103 } 104 } 105 } 106 107 private DefaultMutableTreeNode getRootNode(final String path) { 108 File bundleDir = new File(path); 109 if (bundleDir.isDirectory()) { 110 DefaultMutableTreeNode root = new DefaultMutableTreeNode(bundleDir); 111 return root; 112 } 113 return null; 114 } 115 116 private void useSelectedTreeValue(final JTree tree) { 117 assert tree != null : "cannot use a null JTree"; 118 DefaultMutableTreeNode node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); 119 120 if (node != null) { 121 File f = (File) node.getUserObject(); 122 if (f.isDirectory()) { 123 setValue(defaultValue); 124 } else { 125 setValue(node.toString()); 126 } 127 128 TreePath nodePath = new TreePath(node.getPath()); 129 tree.setSelectionPath(nodePath); 130 tree.scrollPathToVisible(nodePath); 131 } 132 } 133 134 @Override public JPanel getComponent() { 135 JPanel panel = new JPanel(new BorderLayout()); 136 137 final String path = StartupManager.getInstance().getPlatform().getUserBundles(); 138 final DefaultMutableTreeNode root = getRootNode(path); 139 if (root == null) { 140 return panel; 141 } 142 143 final JTree tree = new JTree(root); 144 tree.addTreeSelectionListener(e -> useSelectedTreeValue(tree)); 145 tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 146 JScrollPane scroller = new JScrollPane(tree); 147 148 ToolTipManager.sharedInstance().registerComponent(tree); 149 tree.setCellRenderer(new TreeCellRenderer()); 150 scroller.setPreferredSize(new Dimension(140, 130)); 151 tree.expandRow(0); 152 153 if (selected != null) { 154 TreePath nodePath = new TreePath(selected.getPath()); 155 tree.setSelectionPath(nodePath); 156 tree.scrollPathToVisible(nodePath); 157 } 158 159 final JCheckBox enabled = new JCheckBox("Specify default bundle:", true); 160 enabled.addActionListener(e -> { 161 tree.setEnabled(enabled.isSelected()); 162 if (tree.isEnabled()) { 163 useSelectedTreeValue(tree); 164 } else { 165 setValue(defaultValue); 166 } 167 }); 168 169 // this listener is what creates (and destroys) the bundle tree. 170 // ancestorAdded is triggered when "tree" becomes visible, and 171 // ancestorRemoved is triggered when "tree" is no longer visible. 172 tree.addAncestorListener(new AncestorListener() { 173 @Override public void ancestorAdded(AncestorEvent event) { 174 System.err.println("tree visible! calling exploreDirectory: path='"+path+"' root='"+root+'\''); 175 exploreDirectory(path, root); 176 System.err.println("exploreDirectory finished"); 177 DefaultTreeModel model = (DefaultTreeModel)tree.getModel(); 178 model.reload(); 179 tree.revalidate(); 180 } 181 182 @Override public void ancestorRemoved(AncestorEvent event) { 183 System.err.println("tree hidden!"); 184 root.removeAllChildren(); 185 DefaultTreeModel model = (DefaultTreeModel)tree.getModel(); 186 model.reload(); 187 } 188 189 @Override public void ancestorMoved(AncestorEvent event) { } 190 }); 191 192 panel.add(enabled, BorderLayout.PAGE_START); 193 panel.add(scroller, BorderLayout.PAGE_END); 194 return panel; 195 } 196 197 @Override public String getValue() { 198 return '"' + value + '"'; 199 } 200 201 public String getUnquotedValue() { 202 return value; 203 } 204 205 @Override public void setValue(final String newValue) { 206 value = CLEAN_VALUE_REGEX.matcher(newValue).replaceAll(""); 207 } 208 209 public String toString() { 210 return MakeToString.fromInstance(this) 211 .add("optionId", getOptionId()) 212 .add("value", getValue()).toString(); 213 } 214}