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}