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.chooser;
029    
030    import static javax.swing.GroupLayout.DEFAULT_SIZE;
031    import static javax.swing.GroupLayout.Alignment.BASELINE;
032    import static javax.swing.GroupLayout.Alignment.LEADING;
033    import static javax.swing.GroupLayout.Alignment.TRAILING;
034    import static javax.swing.LayoutStyle.ComponentPlacement.RELATED;
035    import static javax.swing.LayoutStyle.ComponentPlacement.UNRELATED;
036    
037    import java.awt.Dimension;
038    import java.awt.Insets;
039    import java.awt.event.ActionEvent;
040    import java.awt.event.ActionListener;
041    import java.util.ArrayList;
042    import java.util.HashMap;
043    import java.util.List;
044    import java.util.Map;
045    
046    import javax.swing.GroupLayout;
047    import javax.swing.JButton;
048    import javax.swing.JComboBox;
049    import javax.swing.JComponent;
050    import javax.swing.JFileChooser;
051    import javax.swing.JLabel;
052    import javax.swing.JPanel;
053    import javax.swing.filechooser.FileFilter;
054    
055    import org.w3c.dom.Element;
056    
057    import ucar.unidata.idv.IntegratedDataViewer;
058    import ucar.unidata.idv.chooser.IdvChooserManager;
059    import ucar.unidata.util.FileManager;
060    import ucar.unidata.util.GuiUtils;
061    import ucar.unidata.util.Misc;
062    import ucar.unidata.util.PatternFileFilter;
063    import ucar.unidata.util.TwoFacedObject;
064    import ucar.unidata.xml.XmlUtil;
065    import edu.wisc.ssec.mcidasv.Constants;
066    import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
067    import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Position;
068    import edu.wisc.ssec.mcidasv.util.McVGuiUtils.TextColor;
069    import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Width;
070    
071    /**
072     * {@code FileChooser} is another {@literal "UI nicety"} extension. The main
073     * difference is that this class allows {@code choosers.xml} to specify a
074     * boolean attribute, {@code "selectdatasourceid"}. If disabled or not present,
075     * a {@code FileChooser} will behave exactly like a standard 
076     * {@link FileChooser}.
077     * 
078     * <p>If the attribute is present and enabled, the {@code FileChooser}'s 
079     * data source type will automatically select the 
080     * {@link ucar.unidata.data.DataSource} corresponding to the chooser's 
081     * {@code "datasourceid"} attribute.
082     */
083    public class FileChooser extends ucar.unidata.idv.chooser.FileChooser implements Constants {
084    
085        /** 
086         * Chooser attribute that controls selecting the default data source.
087         * @see #selectDefaultDataSource
088         */
089        public static final String ATTR_SELECT_DSID = "selectdatasourceid";
090    
091        /** Default data source ID for this chooser. Defaults to {@code null}. */
092        private final String defaultDataSourceId;
093    
094        /** 
095         * Whether or not to select the data source corresponding to 
096         * {@link #defaultDataSourceId} within the {@link JComboBox} returned by
097         * {@link #getDataSourcesComponent()}. Defaults to {@code false}.
098         */
099        private final boolean selectDefaultDataSource;
100    
101        /**
102         * If there is a default data source ID, get the combo box display value
103         */
104        private String defaultDataSourceName;
105        
106        /** Different subclasses can use the combobox of data source ids */
107        private JComboBox sourceComboBox;
108        
109        /**
110         * Get a handle on the actual file chooser
111         */
112        protected JFileChooser fileChooser;
113        
114        /**
115         * Extending classes may need to manipulate the path
116         */
117        protected String path;
118        
119        /**
120         * The panels that might need to be enabled/disabled
121         */
122        protected JPanel topPanel = new JPanel();
123        protected JPanel centerPanel = new JPanel();
124        protected JPanel bottomPanel = new JPanel();
125        
126        /**
127         * Boolean to tell if the load was initiated from the load button
128         * (as opposed to typing in a filename... we need to capture that)
129         */
130        protected Boolean buttonPressed = false;
131        
132        /**
133         * Get a handle on the IDV
134         */
135        protected IntegratedDataViewer idv = getIdv();
136        
137        /**
138         * Creates a {@code FileChooser} and bubbles up {@code mgr} and 
139         * {@code root} to {@link FileChooser}.
140         * 
141         * @param mgr Global IDV chooser manager.
142         * @param root XML representing this chooser.
143         */
144        public FileChooser(final IdvChooserManager mgr, final Element root) {
145            super(mgr, root);
146    
147            String id = XmlUtil.getAttribute(root, ATTR_DATASOURCEID, (String)null);
148            defaultDataSourceId = (id != null) ? id.toLowerCase() : id;
149    
150            selectDefaultDataSource =
151                XmlUtil.getAttribute(root, ATTR_SELECT_DSID, false);
152            
153        }
154        
155        /**
156         * Label for getDataSourcesComponent selector
157         * @return
158         */
159        protected String getDataSourcesLabel() {
160            return "Data Type:";
161        }
162    
163        /**
164         * Overridden so that McIDAS-V can attempt auto-selecting the default data
165         * source type.
166         */
167        @Override
168        protected JComboBox getDataSourcesComponent() {
169            sourceComboBox = getDataSourcesComponent(true);
170            if (selectDefaultDataSource && defaultDataSourceId != null) {
171                Map<String, Integer> ids = comboBoxContents(sourceComboBox);
172                if (ids.containsKey(defaultDataSourceId)) {
173                    sourceComboBox.setSelectedIndex(ids.get(defaultDataSourceId));
174                    defaultDataSourceName = sourceComboBox.getSelectedItem().toString();
175                    sourceComboBox.setVisible(false);
176                }
177            }
178            return sourceComboBox;
179        }
180    
181        /**
182         * Maps data source IDs to their index within {@code box}. This method is 
183         * only applicable to {@link JComboBox}es created for {@link FileChooser}s.
184         * 
185         * @param box Combo box containing relevant data source IDs and indices. 
186         * 
187         * @return A mapping of data source IDs to their offset within {@code box}.
188         */
189        private static Map<String, Integer> comboBoxContents(final JComboBox box) {
190            assert box != null;
191            Map<String, Integer> map = new HashMap<String, Integer>();
192            for (int i = 0; i < box.getItemCount(); i++) {
193                Object o = box.getItemAt(i);
194                if (!(o instanceof TwoFacedObject))
195                    continue;
196                TwoFacedObject tfo = (TwoFacedObject)o;
197                map.put(TwoFacedObject.getIdString(tfo), i);
198            }
199            return map;
200        }
201        
202        /**
203         * If the dataSources combo box is non-null then
204         * return the data source id the user selected.
205         * Else, return null
206         *
207         * @return Data source id
208         */
209        protected String getDataSourceId() {
210            return getDataSourceId(sourceComboBox);
211        }
212        
213        /**
214         * Get the accessory component
215         *
216         * @return the component
217         */
218        protected JComponent getAccessory() {
219            return GuiUtils.left(
220                GuiUtils.inset(
221                    FileManager.makeDirectoryHistoryComponent(
222                        fileChooser, false), new Insets(13, 0, 0, 0)));
223        }
224    
225        /**
226         * Override the base class method to catch the do load
227         */
228        public void doLoadInThread() {
229            selectFiles(fileChooser.getSelectedFiles(),
230                        fileChooser.getCurrentDirectory());
231        }
232    
233        /**
234         * Override the base class method to catch the do update
235         */
236        public void doUpdate() {
237            fileChooser.rescanCurrentDirectory();
238        }
239        
240        /**
241         * Allow multiple file selection.  Override if necessary.
242         */
243        protected boolean getAllowMultiple() {
244            return true;
245        }
246        
247        /**
248         * Set whether the user has made a selection that contains data.
249         *
250         * @param have   true to set the haveData property.  Enables the
251         *               loading button
252         */
253        public void setHaveData(boolean have) {
254            super.setHaveData(have);
255            updateStatus();
256        }
257        
258        /**
259         * Set the status message appropriately
260         */
261        protected void updateStatus() {
262            super.updateStatus();
263            if(!getHaveData()) {
264                if (getAllowMultiple())
265                    setStatus("Select one or more files");
266                else
267                    setStatus("Select a file"); 
268            }
269        }
270            
271        /**
272         * Get the top components for the chooser
273         *
274         * @param comps  the top component
275         */
276        protected void getTopComponents(List comps) {
277            Element chooserNode = getXmlNode();
278    
279            // Force ATTR_DSCOMP to be false before calling super.getTopComponents
280            // We call getDataSourcesComponent later on
281            boolean dscomp = XmlUtil.getAttribute(chooserNode, ATTR_DSCOMP, true);
282            XmlUtil.setAttributes(chooserNode, new String[] { ATTR_DSCOMP, "false" });
283            super.getTopComponents(comps);
284            if (dscomp) XmlUtil.setAttributes(chooserNode, new String[] { ATTR_DSCOMP, "true" });
285        }
286        
287        /**
288         * Get the top panel for the chooser
289         * @return the top panel
290         */
291        protected JPanel getTopPanel() {
292            List topComps  = new ArrayList();
293            getTopComponents(topComps);
294            if (topComps.size() == 0) return null;
295            JPanel topPanel = GuiUtils.left(GuiUtils.doLayout(topComps, 0, GuiUtils.WT_N, GuiUtils.WT_N));
296            topPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
297            
298            return McVGuiUtils.makeLabeledComponent("Options:", topPanel);
299        }
300        
301        /**
302         * Get the bottom panel for the chooser
303         * @return the bottom panel
304         */
305        protected JPanel getBottomPanel() {
306            return null;
307        }
308            
309        /**
310         * Get the center panel for the chooser
311         * @return the center panel
312         */
313        protected JPanel getCenterPanel() {
314            Element chooserNode = getXmlNode();
315    
316            fileChooser = doMakeFileChooser(path);
317            fileChooser.setPreferredSize(new Dimension(300, 300));
318            fileChooser.setMultiSelectionEnabled(getAllowMultiple());
319            
320            List filters = new ArrayList();
321            String filterString = XmlUtil.getAttribute(chooserNode, ATTR_FILTERS, (String) null);
322    
323            filters.addAll(getDataManager().getFileFilters());
324            if (filterString != null) {
325                filters.addAll(PatternFileFilter.createFilters(filterString));
326            }
327    
328            if ( !filters.isEmpty()) {
329                for (int i = 0; i < filters.size(); i++) {
330                    fileChooser.addChoosableFileFilter((FileFilter) filters.get(i));
331                }
332                fileChooser.setFileFilter(fileChooser.getAcceptAllFileFilter());
333            }
334    
335            JPanel centerPanel;
336            JComponent accessory = getAccessory();
337            if (accessory == null) {
338                centerPanel = GuiUtils.center(fileChooser);
339            } else {
340                centerPanel = GuiUtils.centerRight(fileChooser, GuiUtils.top(accessory));
341            }
342            centerPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
343            setHaveData(false);
344            return McVGuiUtils.makeLabeledComponent("Files:", centerPanel);
345        }
346        
347        private JLabel statusLabel = new JLabel("Status");
348    
349        @Override
350        public void setStatus(String statusString, String foo) {
351            if (statusString == null)
352                statusString = "";
353            statusLabel.setText(statusString);
354        }
355            
356        /**
357         * Create a more McIDAS-V-like GUI layout
358         */
359        protected JComponent doMakeContents() {
360            // Run super.doMakeContents()
361            // It does some initialization on private components that we can't get at
362            JComponent parentContents = super.doMakeContents();
363            Element chooserNode = getXmlNode();
364            
365            path = (String) idv.getPreference(PREF_DEFAULTDIR + getId());
366            if (path == null) {
367                path = XmlUtil.getAttribute(chooserNode, ATTR_PATH, (String) null);
368            }
369            
370            JComponent typeComponent = new JPanel();
371            if (XmlUtil.getAttribute(chooserNode, ATTR_DSCOMP, true)) {
372                typeComponent = getDataSourcesComponent();
373            }
374            if (defaultDataSourceName != null) {
375                typeComponent = new JLabel(defaultDataSourceName);
376                McVGuiUtils.setLabelBold((JLabel)typeComponent, true);
377                McVGuiUtils.setComponentHeight(typeComponent, new JComboBox());
378            }
379                            
380            // Create the different panels... extending classes can override these
381            topPanel = getTopPanel();
382            centerPanel = getCenterPanel();
383            bottomPanel = getBottomPanel();
384            
385            JPanel innerPanel = centerPanel;
386            if (topPanel!=null && bottomPanel!=null)
387                innerPanel = McVGuiUtils.topCenterBottom(topPanel, centerPanel, bottomPanel);
388            else if (topPanel!=null) 
389                innerPanel = McVGuiUtils.topBottom(topPanel, centerPanel, McVGuiUtils.Prefer.BOTTOM);
390            else if (bottomPanel!=null)
391                innerPanel = McVGuiUtils.topBottom(centerPanel, bottomPanel, McVGuiUtils.Prefer.TOP);
392            
393            // Start building the whole thing here
394            JPanel outerPanel = new JPanel();
395    
396            JLabel typeLabel = McVGuiUtils.makeLabelRight(getDataSourcesLabel());
397                    
398            JLabel statusLabelLabel = McVGuiUtils.makeLabelRight("");
399                    
400            McVGuiUtils.setLabelPosition(statusLabel, Position.RIGHT);
401            McVGuiUtils.setComponentColor(statusLabel, TextColor.STATUS);
402            
403            JButton helpButton = McVGuiUtils.makeImageButton(ICON_HELP, "Show help");
404            helpButton.setActionCommand(GuiUtils.CMD_HELP);
405            helpButton.addActionListener(this);
406            
407            JButton refreshButton = McVGuiUtils.makeImageButton(ICON_REFRESH, "Refresh");
408            refreshButton.setActionCommand(GuiUtils.CMD_UPDATE);
409            refreshButton.addActionListener(this);
410            
411            McVGuiUtils.setButtonImage(loadButton, ICON_ACCEPT_SMALL);
412            McVGuiUtils.setComponentWidth(loadButton, Width.DOUBLE);
413            
414            // This is how we know if the action was initiated by a button press
415            loadButton.addActionListener(new ActionListener() {
416                       public void actionPerformed(ActionEvent e) {
417                           buttonPressed = true;
418                           Misc.runInABit(1000, new Runnable() {
419                               public void run() {
420                                   buttonPressed = false;
421                               }
422                           });
423                       }
424                  }
425            );
426    
427            GroupLayout layout = new GroupLayout(outerPanel);
428            outerPanel.setLayout(layout);
429            layout.setHorizontalGroup(
430                layout.createParallelGroup(LEADING)
431                .addGroup(TRAILING, layout.createSequentialGroup()
432                    .addGroup(layout.createParallelGroup(TRAILING)
433                        .addGroup(layout.createSequentialGroup()
434                            .addContainerGap()
435                            .addComponent(helpButton)
436                            .addGap(GAP_RELATED)
437                            .addComponent(refreshButton)
438                            .addPreferredGap(RELATED)
439                            .addComponent(loadButton))
440                            .addGroup(LEADING, layout.createSequentialGroup()
441                            .addContainerGap()
442                            .addGroup(layout.createParallelGroup(LEADING)
443                                .addComponent(innerPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
444                                .addGroup(layout.createSequentialGroup()
445                                    .addComponent(typeLabel)
446                                    .addGap(GAP_RELATED)
447                                    .addComponent(typeComponent))
448                                .addGroup(layout.createSequentialGroup()
449                                    .addComponent(statusLabelLabel)
450                                    .addGap(GAP_RELATED)
451                                    .addComponent(statusLabel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)))))
452                    .addContainerGap())
453            );
454            layout.setVerticalGroup(
455                layout.createParallelGroup(LEADING)
456                .addGroup(layout.createSequentialGroup()
457                    .addContainerGap()
458                    .addGroup(layout.createParallelGroup(BASELINE)
459                        .addComponent(typeLabel)
460                        .addComponent(typeComponent))
461                    .addPreferredGap(UNRELATED)
462                    .addComponent(innerPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
463                    .addPreferredGap(UNRELATED)
464                    .addGroup(layout.createParallelGroup(BASELINE)
465                        .addComponent(statusLabelLabel)
466                        .addComponent(statusLabel))
467                    .addPreferredGap(UNRELATED)
468                    .addGroup(layout.createParallelGroup(BASELINE)
469                        .addComponent(loadButton)
470                        .addComponent(refreshButton)
471                        .addComponent(helpButton))
472                    .addContainerGap())
473            );
474        
475            return outerPanel;
476    
477        }
478        
479    }