001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
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 static edu.wisc.ssec.mcidasv.McIDASV.getStaticMcv;
038
039import java.awt.Dimension;
040import java.awt.Insets;
041import java.awt.event.ActionEvent;
042import java.awt.event.ActionListener;
043
044import java.beans.PropertyChangeListener;
045
046import java.io.IOException;
047
048import java.nio.file.Paths;
049
050import java.util.ArrayList;
051import java.util.HashMap;
052import java.util.List;
053import java.util.Map;
054import java.util.Objects;
055
056import javax.swing.GroupLayout;
057import javax.swing.JButton;
058import javax.swing.JComboBox;
059import javax.swing.JComponent;
060import javax.swing.JFileChooser;
061import javax.swing.JLabel;
062import javax.swing.JPanel;
063import javax.swing.SwingUtilities;
064import javax.swing.filechooser.FileFilter;
065
066import edu.wisc.ssec.mcidasv.McIDASV;
067import edu.wisc.ssec.mcidasv.util.pathwatcher.OnFileChangeListener;
068import org.bushe.swing.event.annotation.AnnotationProcessor;
069import org.w3c.dom.Element;
070
071import org.slf4j.Logger;
072import org.slf4j.LoggerFactory;
073
074import org.bushe.swing.event.annotation.EventTopicSubscriber;
075
076import ucar.unidata.idv.IntegratedDataViewer;
077import ucar.unidata.idv.chooser.IdvChooserManager;
078import ucar.unidata.util.FileManager;
079import ucar.unidata.util.GuiUtils;
080import ucar.unidata.util.Misc;
081import ucar.unidata.util.PatternFileFilter;
082import ucar.unidata.util.TwoFacedObject;
083import ucar.unidata.xml.XmlUtil;
084
085import edu.wisc.ssec.mcidasv.util.pathwatcher.DirectoryWatchService;
086
087import edu.wisc.ssec.mcidasv.Constants;
088import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
089import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Position;
090import edu.wisc.ssec.mcidasv.util.McVGuiUtils.TextColor;
091import edu.wisc.ssec.mcidasv.util.McVGuiUtils.Width;
092
093/**
094 * {@code FileChooser} is another {@literal "UI nicety"} extension. The main
095 * difference is that this class allows {@code choosers.xml} to specify a
096 * boolean attribute, {@code "selectdatasourceid"}. If disabled or not present,
097 * a {@code FileChooser} will behave exactly like a standard 
098 * {@link FileChooser}.
099 * 
100 * <p>If the attribute is present and enabled, the {@code FileChooser}'s 
101 * data source type will automatically select the 
102 * {@link ucar.unidata.data.DataSource} corresponding to the chooser's 
103 * {@code "datasourceid"} attribute.
104 */
105public class FileChooser extends ucar.unidata.idv.chooser.FileChooser
106    implements Constants
107{
108
109    /** Logging object. */
110    private static final Logger logger =
111        LoggerFactory.getLogger(FileChooser.class);
112
113    /** 
114     * Chooser attribute that controls selecting the default data source.
115     * @see #selectDefaultDataSource
116     */
117    public static final String ATTR_SELECT_DSID = "selectdatasourceid";
118
119    /** Default data source ID for this chooser. Defaults to {@code null}. */
120    private final String defaultDataSourceId;
121
122    /** 
123     * Whether or not to select the data source corresponding to 
124     * {@link #defaultDataSourceId} within the {@link JComboBox} returned by
125     * {@link #getDataSourcesComponent()}. Defaults to {@code false}.
126     */
127    private final boolean selectDefaultDataSource;
128
129    /**
130     * If there is a default data source ID, get the combo box display value
131     */
132    private String defaultDataSourceName;
133    
134    /** Different subclasses can use the combobox of data source ids */
135    private JComboBox sourceComboBox;
136    
137    /** Get a handle on the actual file chooser. */
138    protected JFileChooser fileChooser;
139
140    /** Panels that might need to be enabled/disabled. */
141    protected JPanel topPanel = new JPanel();
142    protected JPanel centerPanel = new JPanel();
143    protected JPanel bottomPanel = new JPanel();
144    
145    /**
146     * Boolean to tell if the load was initiated from the load button
147     * (as opposed to typing in a filename... we need to capture that)
148     */
149    protected Boolean buttonPressed = false;
150    
151    /** Get a handle on the IDV. */
152    protected IntegratedDataViewer idv = getIdv();
153
154    /** This is mostly used to preemptively null-out the listener. */
155    protected OnFileChangeListener watchListener;
156
157
158    /**
159     * Creates a {@code FileChooser} and bubbles up {@code mgr} and 
160     * {@code root} to {@link FileChooser}.
161     * 
162     * @param mgr Global IDV chooser manager.
163     * @param root XML representing this chooser.
164     */
165    public FileChooser(final IdvChooserManager mgr, final Element root) {
166        super(mgr, root);
167
168        AnnotationProcessor.process(this);
169
170        String id = XmlUtil.getAttribute(root, ATTR_DATASOURCEID, (String)null);
171        defaultDataSourceId = (id != null) ? id.toLowerCase() : id;
172
173        selectDefaultDataSource =
174            XmlUtil.getAttribute(root, ATTR_SELECT_DSID, false);
175        
176    }
177    
178    /**
179     * Label for {@link #getDataSourcesComponent()} selector.
180     *
181     * @return {@code String} to use as the label for data type selector.
182     */
183    protected String getDataSourcesLabel() {
184        return "Data Type:";
185    }
186
187    /**
188     * Overridden so that McIDAS-V can attempt auto-selecting the default data
189     * source type.
190     */
191    @Override
192    protected JComboBox getDataSourcesComponent() {
193        sourceComboBox = getDataSourcesComponent(true);
194        if (selectDefaultDataSource && defaultDataSourceId != null) {
195            Map<String, Integer> ids = comboBoxContents(sourceComboBox);
196            if (ids.containsKey(defaultDataSourceId)) {
197                sourceComboBox.setSelectedIndex(ids.get(defaultDataSourceId));
198                defaultDataSourceName = sourceComboBox.getSelectedItem().toString();
199                sourceComboBox.setVisible(false);
200            }
201        }
202        return sourceComboBox;
203    }
204
205    /**
206     * Maps data source IDs to their index within {@code box}. This method is 
207     * only applicable to {@link JComboBox}es created for {@link FileChooser}s.
208     * 
209     * @param box Combo box containing relevant data source IDs and indices. 
210     * 
211     * @return A mapping of data source IDs to their offset within {@code box}.
212     */
213    private static Map<String, Integer> comboBoxContents(final JComboBox box) {
214        assert box != null;
215        Map<String, Integer> map = new HashMap<String, Integer>();
216        for (int i = 0; i < box.getItemCount(); i++) {
217            Object o = box.getItemAt(i);
218            if (!(o instanceof TwoFacedObject))
219                continue;
220            TwoFacedObject tfo = (TwoFacedObject)o;
221            map.put(TwoFacedObject.getIdString(tfo), i);
222        }
223        return map;
224    }
225    
226    /**
227     * If the dataSources combo box is non-null then
228     * return the data source id the user selected.
229     * Else, return null
230     *
231     * @return Data source id
232     */
233    protected String getDataSourceId() {
234        return getDataSourceId(sourceComboBox);
235    }
236    
237    /**
238     * Get the accessory component
239     *
240     * @return the component
241     */
242    protected JComponent getAccessory() {
243        return GuiUtils.left(
244            GuiUtils.inset(
245                FileManager.makeDirectoryHistoryComponent(
246                    fileChooser, false), new Insets(13, 0, 0, 0)));
247    }
248
249    /**
250     * Override the base class method to catch the do load
251     */
252    public void doLoadInThread() {
253        selectFiles(fileChooser.getSelectedFiles(),
254                    fileChooser.getCurrentDirectory());
255    }
256
257    /**
258     * Override the base class method to catch the do update
259     */
260    public void doUpdate() {
261        fileChooser.rescanCurrentDirectory();
262    }
263    
264    /**
265     * Allow multiple file selection.  Override if necessary.
266     *
267     * @return Always returns {@code true}.
268     */
269    protected boolean getAllowMultiple() {
270        return true;
271    }
272    
273    /**
274     * Set whether the user has made a selection that contains data.
275     *
276     * @param have   true to set the haveData property.  Enables the
277     *               loading button
278     */
279    public void setHaveData(boolean have) {
280        super.setHaveData(have);
281        updateStatus();
282    }
283    
284    /**
285     * Set the status message appropriately
286     */
287    protected void updateStatus() {
288        super.updateStatus();
289        if(!getHaveData()) {
290            if (getAllowMultiple())
291                setStatus("Select one or more files");
292            else
293                setStatus("Select a file"); 
294        }
295    }
296        
297    /**
298     * Get the top components for the chooser
299     *
300     * @param comps  the top component
301     */
302    protected void getTopComponents(List comps) {
303        Element chooserNode = getXmlNode();
304
305        // Force ATTR_DSCOMP to be false before calling super.getTopComponents
306        // We call getDataSourcesComponent later on
307        boolean dscomp = XmlUtil.getAttribute(chooserNode, ATTR_DSCOMP, true);
308        XmlUtil.setAttributes(chooserNode, new String[] { ATTR_DSCOMP, "false" });
309        super.getTopComponents(comps);
310        if (dscomp) XmlUtil.setAttributes(chooserNode, new String[] { ATTR_DSCOMP, "true" });
311    }
312    
313    /**
314     * Get the top panel for the chooser
315     * @return the top panel
316     */
317    protected JPanel getTopPanel() {
318        List topComps  = new ArrayList();
319        getTopComponents(topComps);
320        if (topComps.size() == 0) return null;
321        JPanel topPanel = GuiUtils.left(GuiUtils.doLayout(topComps, 0, GuiUtils.WT_N, GuiUtils.WT_N));
322        topPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
323        
324        return McVGuiUtils.makeLabeledComponent("Options:", topPanel);
325    }
326    
327    /**
328     * Get the bottom panel for the chooser
329     * @return the bottom panel
330     */
331    protected JPanel getBottomPanel() {
332        return null;
333    }
334        
335    /**
336     * Get the center panel for the chooser
337     * @return the center panel
338     */
339    protected JPanel getCenterPanel() {
340        Element chooserNode = getXmlNode();
341
342        fileChooser = doMakeFileChooser(getPath());
343        fileChooser.setPreferredSize(new Dimension(300, 300));
344        fileChooser.setMultiSelectionEnabled(getAllowMultiple());
345
346        fileChooser.addPropertyChangeListener(
347            JFileChooser.DIRECTORY_CHANGED_PROPERTY,
348            createPropertyListener()
349        );
350        
351        List filters = new ArrayList();
352        String filterString = XmlUtil.getAttribute(chooserNode, ATTR_FILTERS, (String) null);
353
354        filters.addAll(getDataManager().getFileFilters());
355        if (filterString != null) {
356            filters.addAll(PatternFileFilter.createFilters(filterString));
357        }
358
359        if ( !filters.isEmpty()) {
360            for (int i = 0; i < filters.size(); i++) {
361                fileChooser.addChoosableFileFilter((FileFilter) filters.get(i));
362            }
363            fileChooser.setFileFilter(fileChooser.getAcceptAllFileFilter());
364        }
365
366        JPanel centerPanel;
367        JComponent accessory = getAccessory();
368        if (accessory == null) {
369            centerPanel = GuiUtils.center(fileChooser);
370        } else {
371            centerPanel = GuiUtils.centerRight(fileChooser, GuiUtils.top(accessory));
372        }
373        centerPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
374        setHaveData(false);
375        return McVGuiUtils.makeLabeledComponent("Files:", centerPanel);
376    }
377
378    /**
379     * Creates a {@link PropertyChangeListener} that listens for
380     * {@link JFileChooser#DIRECTORY_CHANGED_PROPERTY}.
381     *
382     * <p>This is used to disable directory monitoring in directories not
383     * being looked at, as well as enabling monitoring of the directory the
384     * user has chosen.</p>
385     *
386     * @return {@code PropertyChangeListener} that listens for
387     * {@code JFileChooser} directory changes.
388     */
389    protected PropertyChangeListener createPropertyListener() {
390        return evt -> {
391            logger.trace("prop change: evt={}", evt);
392            String name = evt.getPropertyName();
393            if (JFileChooser.DIRECTORY_CHANGED_PROPERTY.equals(name)) {
394                String newPath = evt.getNewValue().toString();
395                logger.trace("old: '{}', new: '{}'", getPath(), newPath);
396                handleChangeWatchService(newPath);
397            }
398        };
399    }
400
401    /**
402     * Change the path that the file chooser is presenting to the user.
403     *
404     * <p>This value will be written to the user's preferences so that the user
405     * can pick up where they left off after restarting McIDAS-V.</p>
406     *
407     * @param newPath Path to set.
408     */
409    public void setPath(String newPath) {
410        String id = PREF_DEFAULTDIR + getId();
411        idv.getStateManager().writePreference(id, newPath);
412    }
413
414    /**
415     * See the javadoc for {@link #getPath(String)}.
416     *
417     * <p>The difference between the two is that this method passes the value
418     * of {@code System.getProperty("user.home")} to {@link #getPath(String)}
419     * as the default value.</p>
420     *
421     * @return Path to use for the chooser.
422     */
423    public String getPath() {
424        return getPath(System.getProperty("user.home"));
425    }
426
427    /**
428     * Get the path the {@link JFileChooser} should be using.
429     *
430     * <p>If the path in the user's preferences is {@code null}
431     * (or does not exist), {@code defaultValue} will be returned.</p>
432     *
433     * @param defaultValue Default path to use if there is a {@literal "bad"}
434     *                     path in the user's preferences.
435     *                     Cannot be {@code null}.
436     *
437     * @return Path to use for the chooser.
438     *
439     * @throws NullPointerException if {@code defaultValue} is {@code null}.
440     */
441    public String getPath(final String defaultValue) {
442        Objects.requireNonNull(defaultValue, "Default value may not be null");
443        String tempPath = (String)idv.getPreference(PREF_DEFAULTDIR + getId());
444        if ((tempPath == null) || !Paths.get(tempPath).toFile().exists()) {
445            tempPath = defaultValue;
446        }
447        return tempPath;
448    }
449
450    /**
451     * Respond to path changes in the {@code JFileChooser}.
452     *
453     * <p>This method will disable monitoring of the previous path and then
454     * enable monitoring of {@code newPath}.</p>
455     *
456     * @param newPath New path to begin watching.
457     */
458    public void handleChangeWatchService(final String newPath) {
459        DirectoryWatchService watchService = getStaticMcv().getWatchService();
460        if (watchService != null && watchListener != null) {
461            logger.trace("now watching '{}'", newPath);
462
463            setPath(newPath);
464
465            handleStopWatchService(
466                Constants.EVENT_FILECHOOSER_STOP,
467                "changed directory"
468            );
469
470            handleStartWatchService(
471                Constants.EVENT_FILECHOOSER_START,
472                "new directory"
473            );
474        }
475    }
476
477    /**
478     * Begin monitoring the directory returned by {@link #getPath()} for
479     * changes.
480     *
481     * @param topic Artifact from {@code EventBus} annotation. Not used.
482     * @param reason Optional {@literal "Reason"} for starting.
483     *               Helpful for logging.
484     */
485    @EventTopicSubscriber(topic=Constants.EVENT_FILECHOOSER_START)
486    public void handleStartWatchService(final String topic,
487                                        final Object reason)
488    {
489        McIDASV mcv = getStaticMcv();
490        boolean offscreen = mcv.getArgsManager().getIsOffScreen();
491        boolean initDone = mcv.getHaveInitialized();
492        String watchPath = getPath();
493        if (!offscreen && initDone) {
494            try {
495                watchListener = createWatcher();
496                mcv.watchDirectory(watchPath, "*", watchListener);
497                logger.trace("watching '{}' pattern: '{}' (reason: '{}')", watchPath, "*", reason);
498            } catch (IOException e) {
499                logger.error("error creating watch service", e);
500            }
501        }
502    }
503
504    /**
505     * Disable directory monitoring (if it was enabled in the first place).
506     *
507     * @param topic Artifact from {@code EventBus} annotation. Not used.
508     * @param reason Optional {@literal "Reason"} for starting.
509     *               Helpful for logging.
510     */
511    @EventTopicSubscriber(topic=Constants.EVENT_FILECHOOSER_STOP)
512    public void handleStopWatchService(final String topic,
513                                       final Object reason)
514    {
515        logger.trace("stopping service (reason: '{}')", reason);
516
517        DirectoryWatchService service = getStaticMcv().getWatchService();
518        service.unregister(watchListener);
519
520        service = null;
521        watchListener = null;
522        logger.trace("should be good to go!");
523    }
524
525    /**
526     * Creates a directory monitoring
527     * {@link edu.wisc.ssec.mcidasv.util.pathwatcher.Service Service}.
528     *
529     * @return Directory monitor that will respond to changes.
530     */
531    protected OnFileChangeListener createWatcher() {
532        watchListener = new OnFileChangeListener() {
533            @Override public void onFileCreate(String filePath) {
534                logger.trace("file created: '{}'", filePath);
535                DirectoryWatchService service = getStaticMcv().getWatchService();
536                if ((fileChooser != null) && service.isRunning()) {
537                    SwingUtilities.invokeLater(() -> doUpdate());
538                }
539            }
540
541            @Override public void onFileModify(String filePath) {
542                logger.trace("file modified: '{}'", filePath);
543                DirectoryWatchService service = getStaticMcv().getWatchService();
544                if ((fileChooser != null) && service.isRunning()) {
545                    SwingUtilities.invokeLater(() -> doUpdate());
546                }
547            }
548
549            @Override public void onFileDelete(String filePath) {
550                logger.trace("file deleted: '{}'", filePath);
551                DirectoryWatchService service = getStaticMcv().getWatchService();
552                if ((fileChooser != null) && service.isRunning()) {
553                    SwingUtilities.invokeLater(() -> doUpdate());
554                }
555            }
556        };
557        return watchListener;
558    }
559
560    private JLabel statusLabel = new JLabel("Status");
561
562    @Override
563    public void setStatus(String statusString, String foo) {
564        if (statusString == null)
565            statusString = "";
566        statusLabel.setText(statusString);
567    }
568        
569    /**
570     * Create a more McIDAS-V-like GUI layout
571     */
572    protected JComponent doMakeContents() {
573        // Run super.doMakeContents()
574        // It does some initialization on private components that we can't get at
575        JComponent parentContents = super.doMakeContents();
576        Element chooserNode = getXmlNode();
577
578        String pathFromXml =
579            XmlUtil.getAttribute(chooserNode, ATTR_PATH, (String)null);
580        if (pathFromXml != null && Paths.get(pathFromXml).toFile().exists()) {
581            setPath(pathFromXml);
582        }
583
584        JComponent typeComponent = new JPanel();
585        if (XmlUtil.getAttribute(chooserNode, ATTR_DSCOMP, true)) {
586            typeComponent = getDataSourcesComponent();
587        }
588        if (defaultDataSourceName != null) {
589            typeComponent = new JLabel(defaultDataSourceName);
590            McVGuiUtils.setLabelBold((JLabel)typeComponent, true);
591            McVGuiUtils.setComponentHeight(typeComponent, new JComboBox());
592        }
593                        
594        // Create the different panels... extending classes can override these
595        topPanel = getTopPanel();
596        centerPanel = getCenterPanel();
597        bottomPanel = getBottomPanel();
598        
599        JPanel innerPanel = centerPanel;
600        if (topPanel!=null && bottomPanel!=null)
601            innerPanel = McVGuiUtils.topCenterBottom(topPanel, centerPanel, bottomPanel);
602        else if (topPanel!=null) 
603            innerPanel = McVGuiUtils.topBottom(topPanel, centerPanel, McVGuiUtils.Prefer.BOTTOM);
604        else if (bottomPanel!=null)
605            innerPanel = McVGuiUtils.topBottom(centerPanel, bottomPanel, McVGuiUtils.Prefer.TOP);
606        
607        // Start building the whole thing here
608        JPanel outerPanel = new JPanel();
609
610        JLabel typeLabel = McVGuiUtils.makeLabelRight(getDataSourcesLabel());
611                
612        JLabel statusLabelLabel = McVGuiUtils.makeLabelRight("");
613                
614        McVGuiUtils.setLabelPosition(statusLabel, Position.RIGHT);
615        McVGuiUtils.setComponentColor(statusLabel, TextColor.STATUS);
616        
617        JButton helpButton = McVGuiUtils.makeImageButton(ICON_HELP, "Show help");
618        helpButton.setActionCommand(GuiUtils.CMD_HELP);
619        helpButton.addActionListener(this);
620        
621        JButton refreshButton = McVGuiUtils.makeImageButton(ICON_REFRESH, "Refresh");
622        refreshButton.setActionCommand(GuiUtils.CMD_UPDATE);
623        refreshButton.addActionListener(this);
624        
625        McVGuiUtils.setButtonImage(loadButton, ICON_ACCEPT_SMALL);
626        McVGuiUtils.setComponentWidth(loadButton, Width.DOUBLE);
627        
628        // This is how we know if the action was initiated by a button press
629        loadButton.addActionListener(new ActionListener() {
630                   public void actionPerformed(ActionEvent e) {
631                       buttonPressed = true;
632                       Misc.runInABit(1000, new Runnable() {
633                           public void run() {
634                               buttonPressed = false;
635                           }
636                       });
637                   }
638              }
639        );
640
641        GroupLayout layout = new GroupLayout(outerPanel);
642        outerPanel.setLayout(layout);
643        layout.setHorizontalGroup(
644            layout.createParallelGroup(LEADING)
645            .addGroup(TRAILING, layout.createSequentialGroup()
646                .addGroup(layout.createParallelGroup(TRAILING)
647                    .addGroup(layout.createSequentialGroup()
648                        .addContainerGap()
649                        .addComponent(helpButton)
650                        .addGap(GAP_RELATED)
651                        .addComponent(refreshButton)
652                        .addPreferredGap(RELATED)
653                        .addComponent(loadButton))
654                        .addGroup(LEADING, layout.createSequentialGroup()
655                        .addContainerGap()
656                        .addGroup(layout.createParallelGroup(LEADING)
657                            .addComponent(innerPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
658                            .addGroup(layout.createSequentialGroup()
659                                .addComponent(typeLabel)
660                                .addGap(GAP_RELATED)
661                                .addComponent(typeComponent))
662                            .addGroup(layout.createSequentialGroup()
663                                .addComponent(statusLabelLabel)
664                                .addGap(GAP_RELATED)
665                                .addComponent(statusLabel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)))))
666                .addContainerGap())
667        );
668        layout.setVerticalGroup(
669            layout.createParallelGroup(LEADING)
670            .addGroup(layout.createSequentialGroup()
671                .addContainerGap()
672                .addGroup(layout.createParallelGroup(BASELINE)
673                    .addComponent(typeLabel)
674                    .addComponent(typeComponent))
675                .addPreferredGap(UNRELATED)
676                .addComponent(innerPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
677                .addPreferredGap(UNRELATED)
678                .addGroup(layout.createParallelGroup(BASELINE)
679                    .addComponent(statusLabelLabel)
680                    .addComponent(statusLabel))
681                .addPreferredGap(UNRELATED)
682                .addGroup(layout.createParallelGroup(BASELINE)
683                    .addComponent(loadButton)
684                    .addComponent(refreshButton)
685                    .addComponent(helpButton))
686                .addContainerGap())
687        );
688    
689        return outerPanel;
690
691    }
692    
693}