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