001/*
002 * $Id: LocalEntryEditor.java,v 1.18 2011/03/24 16:06:34 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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 */
030package edu.wisc.ssec.mcidasv.servermanager;
031
032import java.awt.Component;
033import java.awt.Container;
034import java.awt.event.ActionEvent;
035import java.awt.event.ActionListener;
036import java.io.File;
037import java.util.Collections;
038import java.util.Set;
039
040import javax.swing.DefaultComboBoxModel;
041import javax.swing.JButton;
042import javax.swing.JComboBox;
043import javax.swing.JFileChooser;
044import javax.swing.JLabel;
045import javax.swing.JList;
046import javax.swing.JTextField;
047import javax.swing.SwingUtilities;
048import javax.swing.WindowConstants;
049import javax.swing.plaf.basic.BasicComboBoxRenderer;
050
051import net.miginfocom.swing.MigLayout;
052
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056import ucar.unidata.xml.XmlObjectStore;
057
058import edu.wisc.ssec.mcidasv.McIDASV;
059import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat;
060import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EditorAction;
061import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
062import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
063import edu.wisc.ssec.mcidasv.util.McVTextField;
064
065/**
066 * A dialog that allows the user to define or modify {@link LocalAddeEntry}s.
067 */
068@SuppressWarnings("serial")
069public class LocalEntryEditor extends javax.swing.JDialog {
070
071    private static final Logger logger = LoggerFactory.getLogger(LocalEntryEditor.class);
072
073    /** Property ID for the last directory selected. */
074    private static final String PROP_LAST_PATH = "mcv.localdata.lastpath";
075
076    /** The valid local ADDE formats. */
077    private static final DefaultComboBoxModel formats = new DefaultComboBoxModel(new Object[] { AddeFormat.MCIDAS_AREA, AddeFormat.AMSRE_L1B, AddeFormat.AMSRE_RAIN_PRODUCT, AddeFormat.GINI, AddeFormat.LRIT_GOES9, AddeFormat.LRIT_GOES10, AddeFormat.LRIT_GOES11, AddeFormat.LRIT_GOES12, AddeFormat.LRIT_MET5, AddeFormat.LRIT_MET7, AddeFormat.LRIT_MTSAT1R, AddeFormat.METEOSAT_OPENMTP, AddeFormat.METOP_AVHRR_L1B, AddeFormat.MODIS_L1B_MOD02, AddeFormat.MODIS_L2_MOD06, AddeFormat.MODIS_L2_MOD07, AddeFormat.MODIS_L2_MOD35, AddeFormat.MODIS_L2_MOD04, AddeFormat.MODIS_L2_MOD28, AddeFormat.MODIS_L2_MODR, AddeFormat.MSG_HRIT_FD, AddeFormat.MSG_HRIT_HRV, AddeFormat.MTSAT_HRIT, AddeFormat.NOAA_AVHRR_L1B, AddeFormat.SSMI, AddeFormat.TRMM });
078
079    /** The server manager GUI. Be aware that this can be {@code null}. */
080    private final TabbedAddeManager managerController;
081
082    /** Reference back to the server manager. */
083    private final EntryStore entryStore;
084
085    private final LocalAddeEntry currentEntry;
086
087    /** Either the path to an ADDE directory as selected by the user or an empty {@link String}. */
088    private String selectedPath = "";
089
090    /** The last dialog action performed by the user. */
091    private EditorAction editorAction = EditorAction.INVALID;
092
093    private final String datasetText;
094
095    /**
096     * Creates a modal local ADDE data editor. It's pretty useful when adding
097     * from a chooser.
098     * 
099     * @param entryStore The server manager. Should not be {@code null}.
100     * @param group Name of the group/dataset containing the desired data. Be aware that {@code null} is okay.
101     */
102    public LocalEntryEditor(final EntryStore entryStore, final String group) {
103        super((javax.swing.JDialog)null, true);
104        this.managerController = null;
105        this.entryStore = entryStore;
106        this.datasetText = group;
107        this.currentEntry = null;
108        SwingUtilities.invokeLater(new Runnable() {
109            @Override public void run() {
110                initComponents(LocalAddeEntry.INVALID_ENTRY);
111            }
112        });
113    }
114
115    // TODO(jon): hold back on javadocs, this is likely to change
116    public LocalEntryEditor(java.awt.Frame parent, boolean modal, final TabbedAddeManager manager, final EntryStore store) {
117        super(manager, modal);
118        this.managerController = manager;
119        this.entryStore = store;
120        this.datasetText = null;
121        this.currentEntry = null;
122        SwingUtilities.invokeLater(new Runnable() {
123            @Override public void run() {
124                initComponents(LocalAddeEntry.INVALID_ENTRY);
125            }
126        });
127    }
128
129    // TODO(jon): hold back on javadocs, this is likely to change
130    public LocalEntryEditor(java.awt.Frame parent, boolean modal, final TabbedAddeManager manager, final EntryStore store, final LocalAddeEntry entry) {
131        super(manager, modal);
132        this.managerController = manager;
133        this.entryStore = store;
134        this.datasetText = null;
135        this.currentEntry = entry;
136        SwingUtilities.invokeLater(new Runnable() {
137            @Override public void run() {
138                initComponents(entry);
139            }
140        });
141    }
142
143    /**
144     * Creates the editor dialog and initializes the various GUI components.
145     * 
146     * @param initEntry Use {@link LocalAddeEntry#INVALID_ENTRY} to specify 
147     * that the user is creating a new entry; otherwise provide the actual
148     * entry that the user is editing.
149     */
150    private void initComponents(final LocalAddeEntry initEntry) {
151        JLabel datasetLabel = new JLabel("Dataset (e.g. MYDATA):");
152        datasetField = McVGuiUtils.makeTextFieldDeny("", 8, true, McVTextField.mcidasDeny);
153        datasetLabel.setLabelFor(datasetField);
154        datasetField.setColumns(20);
155        if (datasetText != null) {
156            datasetField.setText(datasetText);
157        }
158
159        JLabel typeLabel = new JLabel("Image Type (e.g. JAN 07 GOES):");
160        typeField = new JTextField();
161        typeLabel.setLabelFor(typeField);
162        typeField.setColumns(20);
163
164        JLabel formatLabel = new JLabel("Format:");
165        formatComboBox = new JComboBox();
166        formatComboBox.setRenderer(new TooltipComboBoxRenderer());
167        formatComboBox.setModel(formats);
168        formatComboBox.setSelectedIndex(0);
169        formatLabel.setLabelFor(formatComboBox);
170
171        JLabel directoryLabel = new JLabel("Directory:");
172        directoryField = new JTextField();
173        directoryLabel.setLabelFor(directoryField);
174        directoryField.setColumns(20);
175
176        JButton browseButton = new JButton("Browse...");
177        browseButton.addActionListener(new ActionListener() {
178            @Override public void actionPerformed(final ActionEvent evt) {
179                browseButtonActionPerformed(evt);
180            }
181        });
182
183        JButton saveButton = new JButton("Add Dataset");
184        saveButton.addActionListener(new ActionListener() {
185            @Override public void actionPerformed(final ActionEvent evt) {
186                if (initEntry == LocalAddeEntry.INVALID_ENTRY) {
187                    saveButtonActionPerformed(evt);
188                } else {
189                    editButtonActionPerformed(evt);
190                }
191            }
192        });
193
194        JButton cancelButton = new JButton("Cancel");
195        cancelButton.addActionListener(new ActionListener() {
196            @Override public void actionPerformed(final ActionEvent evt) {
197                cancelButtonActionPerformed(evt);
198            }
199        });
200
201        if (initEntry == LocalAddeEntry.INVALID_ENTRY) {
202            setTitle("Add Local Dataset");
203        } else {
204            setTitle("Edit Local Dataset");
205            saveButton.setText("Save Changes");
206            datasetField.setText(initEntry.getGroup());
207            typeField.setText(initEntry.getName());
208            directoryField.setText(EntryTransforms.demungeFileMask(initEntry.getFileMask()));
209            formatComboBox.setSelectedItem(initEntry.getFormat());
210        }
211
212        setResizable(false);
213        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
214        Container c = getContentPane();
215        c.setLayout(new MigLayout(
216            "",                    // general layout constraints; currently
217                                   // none are specified.
218            "[align right][fill]", // column constraints; defined two columns
219                                   // leftmost aligns the components right;
220                                   // rightmost simply fills the remaining space
221            "[][][][][][]"));      // row constraints; possibly not needed in
222                                   // this particular example?
223
224        // done via WindowBuilder + Eclipse
225//        c.add(datasetLabel,   "cell 0 0"); // row: 0; col: 0
226//        c.add(datasetField,   "cell 1 0"); // row: 0; col: 1
227//        c.add(typeLabel,      "cell 0 1"); // row: 1; col: 0
228//        c.add(typeField,      "cell 1 1"); // row: 1; col: 1
229//        c.add(formatLabel,    "cell 0 2"); // row: 2; col: 0
230//        c.add(formatComboBox, "cell 1 2"); // row: 2; col: 1
231//        c.add(directoryLabel, "cell 0 3"); // row: 3; col: 0 ... etc!
232//        c.add(directoryField, "flowx,cell 1 3");
233//        c.add(browseButton,   "cell 1 3,alignx right");
234//        c.add(saveButton,     "flowx,cell 1 5,alignx right,aligny top");
235//        c.add(cancelButton,   "cell 1 5,alignx right,aligny top");
236
237        // another way to accomplish the above layout.
238        c.add(datasetLabel);
239        c.add(datasetField,   "wrap"); // think "newline" or "new row"
240        c.add(typeLabel);
241        c.add(typeField,      "wrap"); // think "newline" or "new row"
242        c.add(formatLabel);
243        c.add(formatComboBox, "wrap"); // think "newline" or "new row"
244        c.add(directoryLabel);
245        c.add(directoryField, "flowx, split 2"); // split this current cell 
246                                                 // into two "subcells"; this
247                                                 // will cause browseButton to
248                                                 // be grouped into the current
249                                                 // cell.
250        c.add(browseButton,   "alignx right, wrap");
251
252        // skips "cell 0 5" causing this row to start in "cell 1 5"; splits 
253        // the cell so that saveButton and cancelButton both occupy cell 1 5.
254        c.add(saveButton,     "flowx, split 2, skip 1, alignx right, aligny top");
255        c.add(cancelButton,   "alignx right, aligny top");
256        pack();
257    }// </editor-fold>
258
259    /**
260     * Triggered when the {@literal "add"} button is clicked.
261     */
262    private void saveButtonActionPerformed(final ActionEvent evt) {
263        addEntry();
264    }
265
266    private void editButtonActionPerformed(final ActionEvent evt) {
267        editEntry();
268    }
269
270    /**
271     * Triggered when the {@literal "file picker"} button is clicked.
272     */
273    private void browseButtonActionPerformed(final ActionEvent evt) {
274        String lastPath = getLastPath();
275        selectedPath = getDataDirectory(lastPath);
276        // yes, the "!=" is intentional! getDataDirectory(String) will return
277        // the exact String it is given if the user cancelled the file picker
278        if (selectedPath != lastPath) {
279            directoryField.setText(selectedPath);
280            setLastPath(selectedPath);
281        }
282    }
283
284    /**
285     * Returns the value of the {@link #PROP_LAST_PATH} McIDAS-V property.
286     * 
287     * @return Either the {@code String} representation of the last path 
288     * selected by the user, or an empty {@code String}.
289     */
290    private String getLastPath() {
291        McIDASV mcv = McIDASV.getStaticMcv();
292        String path = "";
293        if (mcv != null) {
294            return mcv.getObjectStore().get(PROP_LAST_PATH, "");
295        }
296        return path;
297    }
298
299    /**
300     * Sets the value of the {@link #PROP_LAST_PATH} McIDAS-V property to be
301     * the contents of {@code path}.
302     * 
303     * @param path New value for {@link #PROP_LAST_PATH}. {@code null} will be
304     * converted to an empty {@code String}.
305     */
306    public void setLastPath(final String path) {
307        String okayPath = (path != null) ? path : "";
308        McIDASV mcv = McIDASV.getStaticMcv();
309        if (mcv != null) {
310            XmlObjectStore store = mcv.getObjectStore();
311            store.put(PROP_LAST_PATH, okayPath);
312            store.saveIfNeeded();
313        }
314    }
315
316    /**
317     * Calls {@link #dispose} if the dialog is visible.
318     */
319    private void cancelButtonActionPerformed(ActionEvent evt) {
320        if (isDisplayable()) {
321            dispose();
322        }
323    }
324
325    /**
326     * Poll the various UI components and attempt to construct valid ADDE
327     * entries based upon the information provided by the user.
328     * 
329     * @return {@link Set} of entries that represent the user's input, or an
330     * empty {@code Set} if the input was somehow invalid.
331     */
332    private Set<LocalAddeEntry> pollWidgets() {
333        String group = datasetField.getText();
334        String name = typeField.getText();
335        String mask = getLastPath();
336        if (mask.isEmpty() && !directoryField.getText().isEmpty()) {
337            mask = directoryField.getText();
338            setLastPath(mask);
339        }
340        AddeFormat format = (AddeFormat)formatComboBox.getSelectedItem();
341        LocalAddeEntry entry = new LocalAddeEntry.Builder(name, group, mask, format).status(EntryStatus.ENABLED).build();
342        return Collections.singleton(entry);
343    }
344
345    /**
346     * Creates new {@link LocalAddeEntry}s based upon the contents of the dialog
347     * and adds {@literal "them"} to the managed servers. If the dialog is
348     * displayed, we call {@link #dispose()} and attempt to refresh the
349     * server manager GUI if it is available.
350     */
351    private void addEntry() {
352        Set<LocalAddeEntry> addedEntries = pollWidgets();
353        entryStore.addEntries(addedEntries);
354        if (isDisplayable()) {
355            dispose();
356        }
357        if (managerController != null) {
358            managerController.refreshDisplay();
359        }
360    }
361
362    private void editEntry() {
363        Set<LocalAddeEntry> newEntries = pollWidgets();
364        Set<LocalAddeEntry> currentEntries = Collections.singleton(currentEntry);
365        entryStore.replaceEntries(currentEntries, newEntries);
366        if (isDisplayable()) {
367            dispose();
368        }
369        if (managerController != null) {
370            managerController.refreshDisplay();
371        }
372    }
373
374    /**
375     * Ask the user for a data directory from which to create a MASK=
376     * 
377     * @param startDir If this is a valid path, then the file picker will 
378     * (presumably) use that as its initial location. Should not be 
379     * {@code null}?
380     * 
381     * @return Either a path to a data directory or {@code startDir}.
382     */
383    private String getDataDirectory(final String startDir) {
384        JFileChooser fileChooser = new JFileChooser();
385        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
386        fileChooser.setSelectedFile(new File(startDir));
387        switch (fileChooser.showOpenDialog(this)) {
388            case JFileChooser.APPROVE_OPTION:
389                return fileChooser.getSelectedFile().getAbsolutePath();
390            case JFileChooser.CANCEL_OPTION:
391                return startDir;
392            default:
393                return startDir;
394        }
395    }
396
397    /**
398     * @see #editorAction
399     */
400    public EditorAction getEditorAction() {
401        return editorAction;
402    }
403
404    /**
405     * @see #editorAction
406     */
407    private void setEditorAction(final EditorAction editorAction) {
408        this.editorAction = editorAction;
409    }
410
411    /**
412     * Dave's nice combobox tooltip renderer!
413     */
414    private class TooltipComboBoxRenderer extends BasicComboBoxRenderer {
415        @Override public Component getListCellRendererComponent(JList list, 
416            Object value, int index, boolean isSelected, boolean cellHasFocus) 
417        {
418            if (isSelected) {
419                setBackground(list.getSelectionBackground());
420                setForeground(list.getSelectionForeground());
421                if (value != null && (value instanceof AddeFormat))
422                    list.setToolTipText(((AddeFormat)value).getTooltip());
423            } else {
424                setBackground(list.getBackground());
425                setForeground(list.getForeground());
426            }
427            setFont(list.getFont());
428            setText((value == null) ? "" : value.toString());
429            return this;
430        }
431    }
432
433    // Variables declaration - do not modify
434    private JTextField datasetField;
435    private JTextField directoryField;
436    private JComboBox formatComboBox;
437    private JTextField typeField;
438    // End of variables declaration
439}