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