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