001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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 static edu.wisc.ssec.mcidasv.util.McVGuiUtils.sideBySide;
032import static edu.wisc.ssec.mcidasv.util.McVGuiUtils.topBottom;
033
034import java.io.File;
035
036import java.nio.file.Paths;
037
038import java.awt.event.ActionEvent;
039
040import javax.swing.JButton;
041import javax.swing.JCheckBox;
042import javax.swing.JComponent;
043import javax.swing.JFileChooser;
044import javax.swing.JPanel;
045import javax.swing.JTextField;
046import javax.swing.SwingUtilities;
047
048import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
049import edu.wisc.ssec.mcidasv.util.MakeToString;
050import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
051
052/**
053 * Represents a file selection.
054 */
055public final class FileOption extends AbstractOption {
056
057    /** Label for {@link #browseButton}. */
058    private static final String BUTTON_LABEL = "Browse...";
059
060    /** Label for {@link #enableCheckBox}. */
061    private static final String CHECKBOX_LABEL = "Specify default bundle:";
062
063    /** System property that points to the McIDAS-V user path. */
064    private static final String USERPATH = "mcv.userpath";
065
066    /** Name of the {@literal "bundle"} subdirectory of the user path. */
067    private static final String BUNDLE_DIR = "bundles";
068
069    /** Constant that represents string version of the {@code 1} boolean. */
070    private static final String TRUE_STRING = "1";
071
072    /** Constant that represents string version of the {@code 0} boolean. */
073    private static final String FALSE_STRING = "0";
074    
075    /** Used to ensure that no quote marks are present. */
076    private static final String QUOTE = "\"";
077    
078    /** Tool tip used by {@link #bundleField}. */
079    public static final String BUNDLE_FIELD_TIP =
080        "Path to default bundle. An empty path signifies that there is no"
081        + " default bundle in use.";
082
083    /** Default state of {@link #enableCheckBox}. */
084    private final boolean defaultCheckBox;
085
086    /** Default path for {@link #bundleField}. */
087    private final String defaultBundle;
088
089    /**
090     * Shows current default bundle. Empty means there isn't one. May be
091     * {@code null}!
092     */
093    private JTextField bundleField;
094
095    /** Used to pop up a {@link JFileChooser}. May be {@code null}! */
096    private JButton browseButton;
097
098    /**
099     * Whether or not the default bundle should be used. May be {@code null}!
100     */
101    private JCheckBox enableCheckBox;
102
103    /** Current state of {@link #enableCheckBox}. */
104    private boolean checkbox;
105
106    /** Current contents of {@link #bundleField}. Value may be {@code null}! */
107    private String path;
108
109    /**
110     * Create a new {@literal "file option"} that allows the user to select
111     * a file.
112     *
113     * @param id Option ID.
114     * @param label Option label (used in GUI).
115     * @param defaultValue Default option value.
116     * @param platform Platform restrictions for the option.
117     * @param visibility Visibility restrictions for the option.
118     */
119    public FileOption(
120        final String id,
121        final String label,
122        final String defaultValue,
123        final OptionMaster.OptionPlatform platform,
124        final OptionMaster.Visibility visibility)
125    {
126        super(id, label, OptionMaster.Type.DIRTREE, platform, visibility);
127        String[] defaults = parseFormat(defaultValue);
128        this.defaultCheckBox = booleanFromFormat(defaults[0]);
129        this.defaultBundle = defaults[1];
130        setValue(defaultValue);
131    }
132
133    /**
134     * Handles the user clicking on the {@link #browseButton}.
135     *
136     * @param event Currently ignored.
137     */
138    private void browseButtonActionPerformed(ActionEvent event) {
139        String defaultPath =
140            StartupManager.getInstance().getPlatform().getUserBundles();
141        String userPath = System.getProperty(USERPATH, defaultPath);
142        String bundlePath = Paths.get(userPath, BUNDLE_DIR).toString();
143        setValue(selectBundle(bundlePath));
144    }
145
146    /**
147     * Show a {@code JFileChooser} dialog that allows the user to select a
148     * bundle.
149     *
150     * @param bundleDirectory Initial directory for the {@code JFileChooser}.
151     *
152     * @return Either the path to the user's chosen bundle, or
153     * {@link #defaultValue} if the user cancelled.
154     */
155    private String selectBundle(final String bundleDirectory) {
156        JFileChooser fileChooser = new JFileChooser();
157        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
158        if ((path != null) && !path.isEmpty()) {
159            fileChooser.setSelectedFile(new File(path));
160        } else {
161            fileChooser.setCurrentDirectory(new File(bundleDirectory));
162        }
163        String result;
164        switch (fileChooser.showOpenDialog(null)) {
165            case JFileChooser.APPROVE_OPTION:
166                result = fileChooser.getSelectedFile().getAbsolutePath();
167                break;
168            default:
169                result = path;
170                break;
171        }
172        return '"' + getCheckBoxValue() + ';' + result + '"';
173    }
174
175    /**
176     * Returns the GUI component that represents the option.
177     *
178     * @return GUI representation of this option.
179     */
180    @Override public JComponent getComponent() {
181        bundleField = new JTextField(path);
182        bundleField.setColumns(30);
183        bundleField.setToolTipText(BUNDLE_FIELD_TIP);
184        bundleField.setEnabled(checkbox);
185
186        browseButton = new JButton(BUTTON_LABEL);
187        browseButton.setEnabled(checkbox);
188        browseButton.addActionListener(this::browseButtonActionPerformed);
189
190        enableCheckBox = new JCheckBox(CHECKBOX_LABEL, checkbox);
191        enableCheckBox.addActionListener(e -> {
192            boolean status = enableCheckBox.isSelected();
193            bundleField.setEnabled(status);
194            browseButton.setEnabled(status);
195        });
196        JPanel bottom = sideBySide(bundleField, browseButton);
197        return topBottom(enableCheckBox, bottom, McVGuiUtils.Prefer.NEITHER);
198    }
199
200    /**
201     * Returns a string containing the state of {@link #enableCheckBox} and
202     * {@link #bundleField}.
203     *
204     * <p>Results should look like {@code 0;/path/to/bundle.mcv}.</p>
205     *
206     * @return Current value of the option.
207     */
208    @Override public String getValue() {
209        return '"' + getCheckBoxValue() + ';' + getBundlePath() + '"';
210    }
211
212    /**
213     * Returns a string representation of {@link #enableCheckBox}.
214     *
215     * @return Either {@code 1} or {@code 0} depending upon the state of
216     * {@link #enableCheckBox}.
217     */
218    public String getCheckBoxValue() {
219        boolean status = defaultCheckBox;
220        if (enableCheckBox != null) {
221            status = enableCheckBox.isSelected();
222        }
223        return status ? TRUE_STRING : FALSE_STRING;
224    }
225
226    /**
227     * Returns a string representating the path to the startup bundle.
228     *
229     * @return If {@link #bundleField} is {@code null}, {@link #defaultBundle}
230     * is returned. Otherwise the contents of the text field are returned.
231     */
232    public String getBundlePath() {
233        String result = defaultBundle;
234        if (bundleField != null) {
235            result = bundleField.getText();
236        }
237        return result;
238    }
239
240    /**
241     * Forces the value of the option to the data specified.
242     *
243     * @param newValue New value to use.
244     */
245    @Override public void setValue(final String newValue) {
246        String[] results = parseFormat(newValue);
247        checkbox = booleanFromFormat(results[0]);
248        path = results[1];
249        SwingUtilities.invokeLater(() -> {
250            String[] results1 = parseFormat(newValue);
251            checkbox = booleanFromFormat(results1[0]);
252            path = results1[1];
253            if (enableCheckBox != null) {
254                enableCheckBox.setSelected(checkbox);
255            }
256
257            // defaultValue check is to avoid blanking out the field
258            // when the user hits cancel
259            if ((bundleField != null) && !defaultBundle.equals(path)) {
260                bundleField.setEnabled(checkbox);
261                bundleField.setText(path);
262            }
263
264            if (browseButton != null) {
265                browseButton.setEnabled(checkbox);
266            }
267        });
268    }
269
270    /**
271     * Friendly string representation of the option.
272     *
273     * @return {@code String} containing relevant info about the option.
274     */
275    @Override public String toString() {
276        return MakeToString.fromInstance(this)
277                           .add("optionId", getOptionId())
278                           .add("value", getValue()).toString();
279    }
280
281    /**
282     * Attempt to extract something sensible from the value given in
283     * {@literal "runMcV-Prefs"}.
284     *
285     * <p>Expected format is something like {@code "0;/path/to/bundle.mcv"} or
286     * {@code "1;"}. The first example would signify that
287     * {@link #enableCheckBox} is not selected, and the contents of
288     * {@link #bundleField} are {@code /path/to/bundle.mcv}. The second
289     * example would signify that {@link #enableCheckBox} is selected, and the
290     * contents of {@link #bundleField} should be an empty string.</p>
291     *
292     * @param format See method description for details. {@code null} not
293     * allowed.
294     *
295     * @return Two element array where the first element is the state of
296     * {@link #enableCheckBox} and the second is the bundle path. Note that
297     * the bundle path may be empty.
298     *
299     * @see #booleanFromFormat(String)
300     */
301    public static String[] parseFormat(String format) {
302        if (format.startsWith(QUOTE) || format.endsWith(QUOTE)) {
303            format = format.replace(QUOTE, "");
304        }
305        String checkBox = TRUE_STRING;
306        String path;
307        int splitAt = format.indexOf(';');
308        if (splitAt == -1) {
309            // string was something like "/path/goes/here.mcv"
310            path = format;
311        } else if (splitAt == 0) {
312            // string was something like ";/path/goes/here.mcv"
313            path = format.substring(1);
314        } else {
315            // string was something like "1;/path/goes/here.mcv"
316            checkBox = format.substring(0, splitAt);
317            path = format.substring(splitAt + 1);
318        }
319        if (path.isEmpty()) {
320            checkBox = FALSE_STRING;
321        }
322        return new String[] { checkBox, path };
323    }
324
325    /**
326     * Convert the strings {@code 1} and {@code 0} to their corresponding
327     * boolean values.
328     *
329     * @param value String to convert. {@code null} or empty strings accepted.
330     *
331     * @return Returns {@code true} if {@code value} is {@code 1}. Otherwise
332     * returns {@code false}.
333     */
334    public static boolean booleanFromFormat(String value) {
335        boolean result = false;
336        if (TRUE_STRING.equals(value)) {
337            result = true;
338        }
339        return result;
340    }
341}