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