001/*
002 * $Id: FileChooser.java,v 1.12 2011/03/24 16:06:31 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.chooser;
031
032import static javax.swing.GroupLayout.DEFAULT_SIZE;
033import static javax.swing.GroupLayout.Alignment.BASELINE;
034import static javax.swing.GroupLayout.Alignment.LEADING;
035import static javax.swing.GroupLayout.Alignment.TRAILING;
036import static javax.swing.LayoutStyle.ComponentPlacement.RELATED;
037import static javax.swing.LayoutStyle.ComponentPlacement.UNRELATED;
038
039import java.awt.Dimension;
040import java.awt.Insets;
041import java.awt.event.ActionEvent;
042import java.awt.event.ActionListener;
043import java.util.ArrayList;
044import java.util.HashMap;
045import java.util.List;
046import java.util.Map;
047
048import javax.swing.GroupLayout;
049import javax.swing.JButton;
050import javax.swing.JComboBox;
051import javax.swing.JComponent;
052import javax.swing.JFileChooser;
053import javax.swing.JLabel;
054import javax.swing.JPanel;
055import javax.swing.filechooser.FileFilter;
056
057import org.w3c.dom.Element;
058
059import ucar.unidata.idv.IntegratedDataViewer;
060import ucar.unidata.idv.chooser.IdvChooserManager;
061import ucar.unidata.util.FileManager;
062import ucar.unidata.util.GuiUtils;
063import ucar.unidata.util.Misc;
064import ucar.unidata.util.PatternFileFilter;
065import ucar.unidata.util.TwoFacedObject;
066import ucar.unidata.xml.XmlUtil;
067import edu.wisc.ssec.mcidasv.Constants;
068import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
069import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Position;
070import edu.wisc.ssec.mcidasv.util.McVGuiUtils.TextColor;
071import 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 */
085public 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}