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.servermanager;
029    
030    import java.awt.Component;
031    import java.awt.Container;
032    import java.awt.event.ActionEvent;
033    import java.awt.event.ActionListener;
034    import java.io.File;
035    import java.util.Collections;
036    import java.util.Set;
037    
038    import javax.swing.DefaultComboBoxModel;
039    import javax.swing.JButton;
040    import javax.swing.JComboBox;
041    import javax.swing.JFileChooser;
042    import javax.swing.JLabel;
043    import javax.swing.JList;
044    import javax.swing.JTextField;
045    import javax.swing.SwingUtilities;
046    import javax.swing.WindowConstants;
047    import javax.swing.plaf.basic.BasicComboBoxRenderer;
048    
049    import net.miginfocom.swing.MigLayout;
050    
051    import org.slf4j.Logger;
052    import org.slf4j.LoggerFactory;
053    
054    import ucar.unidata.xml.XmlObjectStore;
055    
056    import edu.wisc.ssec.mcidasv.McIDASV;
057    import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat;
058    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EditorAction;
059    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
060    import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
061    import edu.wisc.ssec.mcidasv.util.McVTextField;
062    
063    /**
064     * A dialog that allows the user to define or modify {@link LocalAddeEntry}s.
065     * 
066     * Temporary solution for adding entries via the adde choosers.
067     */
068    @SuppressWarnings("serial")
069    public class LocalEntryShortcut extends javax.swing.JDialog {
070    
071        private static final Logger logger = LoggerFactory.getLogger(LocalEntryShortcut.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, AddeFormat.MCIDAS_MD });
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 LocalEntryShortcut(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 LocalEntryShortcut(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 LocalEntryShortcut(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    }