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 */ 028 029package edu.wisc.ssec.mcidasv.chooser; 030 031import static javax.swing.GroupLayout.DEFAULT_SIZE; 032import static javax.swing.GroupLayout.Alignment.LEADING; 033import static javax.swing.GroupLayout.Alignment.TRAILING; 034 035import java.awt.Component; 036import java.awt.Dimension; 037import java.awt.event.ActionListener; 038import java.io.File; 039import java.util.ArrayList; 040import java.util.Hashtable; 041import java.util.List; 042 043import javax.swing.GroupLayout; 044import javax.swing.JCheckBox; 045import javax.swing.JComponent; 046import javax.swing.JFileChooser; 047import javax.swing.JLabel; 048import javax.swing.JPanel; 049import javax.swing.JRadioButton; 050import javax.swing.JTextField; 051import javax.swing.SwingUtilities; 052 053import edu.wisc.ssec.mcidasv.Constants; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056import org.w3c.dom.Element; 057 058import ucar.unidata.data.DataSource; 059import ucar.unidata.idv.chooser.IdvChooserManager; 060import ucar.unidata.util.FileManager; 061import ucar.unidata.util.GuiUtils; 062import ucar.unidata.util.Misc; 063import ucar.unidata.util.PollingInfo; 064import ucar.unidata.xml.XmlUtil; 065 066import edu.wisc.ssec.mcidasv.ArgumentManager; 067import edu.wisc.ssec.mcidasv.util.McVGuiUtils; 068 069/** 070 * A class for choosing files that can be polled. 071 * 072 * @author IDV development team 073 */ 074public class PollingFileChooser extends FileChooser { 075 076 /** Logging object. */ 077 private static final Logger logger = 078 LoggerFactory.getLogger(PollingFileChooser.class); 079 080 /** Any initial file system path to start with */ 081 public static final String ATTR_DIRECTORY = "directory"; 082 083 /** Polling interval */ 084 public static final String ATTR_INTERVAL = "interval"; 085 086 /** The title attribute */ 087 public static final String ATTR_TITLE = "title"; 088 089 /** polling info */ 090 private PollingInfo pollingInfo; 091 092 /** file path widget accessible to everyone */ 093 private JTextField filePathWidget; 094 095 /** file pattern widget accessible to everyone */ 096 private JTextField filePatternWidget; 097 098 /** Reference to the {@literal "refresh"} checkbox. */ 099 private JCheckBox pollingCbx; 100 101 /** Keep track of what was selected and update status accordingly */ 102 boolean isDirectory = false; 103 int directoryCount = 0; 104 int fileCount = 0; 105 106 /** 107 * Create the PollingFileChooser, passing in the manager and the xml element 108 * from choosers.xml 109 * 110 * @param mgr The manager 111 * @param root The xml root 112 * 113 */ 114 public PollingFileChooser(IdvChooserManager mgr, Element root) { 115 super(mgr, root); 116 117 Element chooserNode = getXmlNode(); 118 119 pollingInfo = (PollingInfo) idv.getPreference(PREF_POLLINGINFO + "." + getId()); 120 if (pollingInfo == null) { 121 pollingInfo = new PollingInfo(); 122 pollingInfo.setMode(PollingInfo.MODE_COUNT); 123 pollingInfo.setName("Directory"); 124 pollingInfo.setFilePattern(getAttribute(ATTR_FILEPATTERN, "")); 125 pollingInfo.setFilePaths(Misc.newList(getAttribute(ATTR_DIRECTORY, ""))); 126 pollingInfo.setIsActive(XmlUtil.getAttribute(chooserNode, ATTR_POLLON, true)); 127 128 pollingInfo.setInterval((long) (XmlUtil.getAttribute(chooserNode, ATTR_INTERVAL, 5.0) * 60 * 1000)); 129 int fileCount = 1; 130 String s = XmlUtil.getAttribute(chooserNode, ATTR_FILECOUNT, "1"); 131 s = s.trim(); 132 if (s.equals("all")) { 133 fileCount = Integer.MAX_VALUE; 134 } else { 135 fileCount = new Integer(s).intValue(); 136 } 137 pollingInfo.setFileCount(fileCount); 138 } 139 filePathWidget = pollingInfo.getFilePathWidget(); 140 filePatternWidget = pollingInfo.getPatternWidget(); 141 142 } 143 144 /** 145 * An extension of JFileChooser 146 * 147 * @author IDV Development Team 148 * @version $Revision$ 149 */ 150 public class MyDirectoryChooser extends MyFileChooser { 151 152 /** 153 * Create the file chooser 154 * 155 * @param path the initial path 156 */ 157 public MyDirectoryChooser(String path) { 158 super(path); 159 setMultiSelectionEnabled(getAllowMultiple()); 160 setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); 161 } 162 163 /** 164 * Set the selected directory 165 * 166 * @param selectedDirectory the selected directory 167 */ 168 public void setCurrentDirectory(File selectedDirectory) { 169 super.setCurrentDirectory(selectedDirectory); 170 setSelectedFiles(null); 171 } 172 173 /** 174 * Set the selected files 175 * 176 * @param selectedFiles the selected files 177 */ 178 public void setSelectedFiles(File[] selectedFiles) { 179 fileCount=0; 180 directoryCount=0; 181 if (selectedFiles == null || selectedFiles.length == 0) { 182 isDirectory = true; 183 if (filePathWidget!=null) { 184 filePathWidget.setText(this.getCurrentDirectory().getAbsolutePath()); 185 } 186 } 187 else { 188 isDirectory = false; 189 for (File selectedFile : selectedFiles) { 190 if (selectedFile.isFile()) fileCount++; 191 if (selectedFile.isDirectory()) { 192 directoryCount++; 193 if (directoryCount==1 && filePathWidget!=null) { 194 filePathWidget.setText(selectedFile.getAbsolutePath()); 195 } 196 } 197 } 198 } 199 super.setSelectedFiles(selectedFiles); 200 201 // Disable load button if we arrived here by typing a directory or file name 202 if (directoryCount > 0 || 203 directoryCount == 0 && fileCount == 0 && !isDirectory) { 204 setHaveData(false); 205 } 206 else { 207 setHaveData(true); 208 } 209 210 updateStatus(); 211 } 212 213 } 214 215 /** 216 * Make the file chooser 217 * 218 * @param path the initial path 219 * 220 * @return the file chooser 221 */ 222 protected JFileChooser doMakeDirectoryChooser(String path) { 223 return new MyDirectoryChooser(path); 224 } 225 226 /** 227 * Override the base class method to catch the do load 228 * This directly handles loading directories and passes off files to selectFiles() and selectFilesInner() 229 */ 230 public void doLoadInThread() { 231 Element chooserNode = getXmlNode(); 232 233 idv.getStateManager().writePreference(PREF_POLLINGINFO + "." + getId(), pollingInfo); 234 idv.getStateManager().writePreference(PREF_DEFAULTDIR + getId(), pollingInfo.getFile()); 235 236// userMessage("doLoadInThread: fileCount: " + fileCount + ", directoryCount: " + directoryCount + ", isDirectory: " + isDirectory + ", getHaveData: " + getHaveData() + ", buttonPressed: " + buttonPressed); 237 238 // If a user types in a directory (on Windows) do not try to load that directory. 239 // If the load button was pressed, go for it! 240 if (fileCount == 0 && !buttonPressed) return; 241 242 // If this is file(s) only, use FileChooser.doLoadInThread() 243 if (fileCount > 0) { 244 super.doLoadInThread(); 245 return; 246 } 247 248 Hashtable properties = new Hashtable(); 249 if ( !pollingInfo.applyProperties()) { 250 return; 251 } 252 253 String title = basename(pollingInfo.getFile()); 254 title += "/" + ((JTextField)pollingInfo.getPatternWidget()).getText(); 255 pollingInfo.setName(title); 256 properties.put(DataSource.PROP_TITLE, title); 257 properties.put(DataSource.PROP_POLLINFO, pollingInfo.cloneMe()); 258 259 String dataSourceId; 260 if (XmlUtil.hasAttribute(chooserNode, ATTR_DATASOURCEID)) { 261 dataSourceId = XmlUtil.getAttribute(chooserNode, ATTR_DATASOURCEID); 262 } else { 263 dataSourceId = getDataSourceId(); 264 } 265 266 makeDataSource(pollingInfo.getFiles(), dataSourceId, properties); 267 } 268 269 /** 270 * Handle the selection of the set of files 271 * Copy from IDV FileChooser, add ability to name and poll 272 */ 273 protected boolean selectFilesInner(File[] files, File directory) 274 throws Exception { 275 Element chooserNode = getXmlNode(); 276 277 if ((files == null) || (files.length == 0)) { 278 userMessage("Please select a file"); 279 return false; 280 } 281 FileManager.addToHistory(files[0]); 282 List selectedFiles = new ArrayList(); 283 String fileNotExistsError = ""; 284 boolean didXidv = false; 285 286 for (int i = 0; i < files.length; i++) { 287 if ( !files[i].exists()) { 288 fileNotExistsError += "File does not exist: " + files[i] + "\n"; 289 } else { 290 String filename = files[i].toString(); 291 //Check for the bundle or jnlp file 292 if (((ArgumentManager)idv.getArgsManager()).isBundle(filename) 293 || idv.getArgsManager().isJnlpFile(filename)) { 294 didXidv = idv.handleAction(filename, null); 295 } else { 296 selectedFiles.add(filename); 297 } 298 } 299 } 300 301 if (didXidv) { 302 closeChooser(); 303 return true; 304 } 305 306 if (selectedFiles.size() == 0) { 307 return false; 308 } 309 310 if (fileNotExistsError.length() > 0) { 311 userMessage(fileNotExistsError); 312 return false; 313 } 314 315 Object definingObject = selectedFiles; 316 String title = selectedFiles.size() + " files"; 317 if (selectedFiles.size() == 1) { 318 definingObject = selectedFiles.get(0); 319 title = basename(definingObject.toString()); 320 } 321 322 String dataSourceId; 323 if (XmlUtil.hasAttribute(chooserNode, ATTR_DATASOURCEID)) { 324 dataSourceId = XmlUtil.getAttribute(chooserNode, ATTR_DATASOURCEID); 325 } else { 326 dataSourceId = getDataSourceId(); 327 } 328 329 Hashtable properties = new Hashtable(); 330 331 // TODO: I disabled file polling on purpose: 332 // The control for this is in the Directory options and is grayed out 333 // when selecting single files. Is this something people want? 334 PollingInfo newPollingInfo = new PollingInfo(false); 335 String pattern = getFilePattern(); 336 if ((pattern != null) && (pattern.length() > 0)) { 337 newPollingInfo.setFilePattern(pattern); 338 } 339 newPollingInfo.setName(title); 340 properties.put(DataSource.PROP_TITLE, title); 341 properties.put(DataSource.PROP_POLLINFO, newPollingInfo); 342 343 // explicitly denote whether or not this was a "bulk load". these data 344 // sources require a little extra attention when being unpersisted. 345 properties.put("bulk.load", (selectedFiles.size() > 1)); 346 return makeDataSource(definingObject, dataSourceId, properties); 347 } 348 349 /** 350 * Emulate basename() 351 */ 352 private String basename(String path) { 353 if (path.lastIndexOf('/') > 0) 354 path = path.substring(path.lastIndexOf('/')); 355 else if (path.lastIndexOf('\\') > 0) 356 path = path.substring(path.lastIndexOf('\\')); 357 if (path.length() > 1) 358 path = path.substring(1); 359 return path; 360 } 361 362 /** 363 * Get the tooltip for the load button 364 * 365 * @return The tooltip for the load button 366 */ 367 protected String getLoadToolTip() { 368 return "Load the file(s) or directory"; 369 } 370 371 /** 372 * Override the base class method to catch the do update. 373 */ 374 @Override public void doUpdate() { 375 fileChooser.rescanCurrentDirectory(); 376 } 377 378 /** 379 * Process PollingInfo GUI components based on their label and properties 380 * Turn it into a nicely-formatted labeled panel 381 */ 382 private JPanel processPollingOption(JLabel label, JPanel panel) { 383 String string = label.getText().trim(); 384 385 // File Pattern 386 if (string.equals("File Pattern:")) { 387 Component panel1 = panel.getComponent(0); 388 if (panel1 instanceof JPanel) { 389 Component[] comps = ((JPanel)panel1).getComponents(); 390 if (comps.length == 2) { 391 List newComps1 = new ArrayList(); 392 List newComps2 = new ArrayList(); 393 if (comps[0] instanceof JPanel) { 394 Component[] comps2 = ((JPanel)comps[0]).getComponents(); 395 if (comps2.length==1 && 396 comps2[0] instanceof JPanel) 397 comps2=((JPanel)comps2[0]).getComponents(); 398 if (comps2.length == 2) { 399 if (comps2[0] instanceof JTextField) { 400 McVGuiUtils.setComponentWidth((JTextField) comps2[0], McVGuiUtils.Width.SINGLE); 401 } 402 newComps1.add(comps2[0]); 403 newComps2.add(comps2[1]); 404 } 405 } 406 newComps1.add(comps[1]); 407 panel = GuiUtils.vbox( 408 GuiUtils.left(GuiUtils.hbox(newComps1)), 409 GuiUtils.left(GuiUtils.hbox(newComps2)) 410 ); 411 } 412 } 413 } 414 415 // Files 416 if (string.equals("Files:")) { 417 Component panel1 = panel.getComponent(0); 418 if (panel1 instanceof JPanel) { 419 Component[] comps = ((JPanel)panel1).getComponents(); 420 if (comps.length == 6) { 421 List newComps1 = new ArrayList(); 422 List newComps2 = new ArrayList(); 423 if (comps[3] instanceof JRadioButton) { 424 String text = ((JRadioButton) comps[3]).getText().trim(); 425 if (text.equals("All files in last:")) text="All files in last"; 426 ((JRadioButton) comps[3]).setText(text); 427 } 428 if (comps[4] instanceof JTextField) { 429 McVGuiUtils.setComponentWidth((JTextField) comps[4], McVGuiUtils.Width.HALF); 430 } 431 if (comps[5] instanceof JLabel) { 432 String text = ((JLabel) comps[5]).getText().trim(); 433 ((JLabel) comps[5]).setText(text); 434 } 435 newComps1.add(comps[0]); 436 newComps1.add(comps[1]); 437 newComps2.add(comps[3]); 438 newComps2.add(comps[4]); 439 newComps2.add(comps[5]); 440 panel = GuiUtils.vbox( 441 GuiUtils.left(GuiUtils.hbox(newComps1)), 442 GuiUtils.left(GuiUtils.hbox(newComps2)) 443 ); 444 } 445 } 446 } 447 448 // Polling 449 if (string.equals("Polling:")) { 450 Component panel1 = panel.getComponent(0); 451 if (panel1 instanceof JPanel) { 452 Component[] comps = ((JPanel)panel1).getComponents(); 453 if (comps.length == 4) { 454 List newComps = new ArrayList(); 455 if (comps[0] instanceof JCheckBox) { 456 ((JCheckBox) comps[0]).setText(""); 457 pollingCbx = (JCheckBox)comps[0]; 458 ActionListener listener = buildPollingActionListener(); 459 pollingCbx.addActionListener(listener); 460 } 461 if (comps[1] instanceof JLabel) { 462 String text = ((JLabel) comps[1]).getText().trim(); 463 if (text.equals("Check every:")) text="Refresh every"; 464 ((JLabel) comps[1]).setText(text); 465 } 466 if (comps[2] instanceof JTextField) { 467 McVGuiUtils.setComponentWidth((JTextField) comps[2], McVGuiUtils.Width.HALF); 468 } 469 if (comps[3] instanceof JLabel) { 470 String text = ((JLabel) comps[3]).getText().trim(); 471 ((JLabel) comps[3]).setText(text); 472 } 473 newComps.add(comps[0]); 474 newComps.add(comps[1]); 475 newComps.add(comps[2]); 476 newComps.add(comps[3]); 477 string=""; 478 panel = GuiUtils.left(GuiUtils.hbox(newComps)); 479 } 480 } 481 } 482 483 return McVGuiUtils.makeLabeledComponent(string, panel); 484 } 485 486 /** 487 * Turn PollingInfo options into a nicely-formatted panel 488 */ 489 private JPanel processPollingOptions(List comps) { 490 List newComps = new ArrayList(); 491 newComps = new ArrayList(); 492 if (comps.size() == 4) { 493// newComps.add(comps.get(0)); 494 495 // Put Recent and Pattern panels next to each other and make them bordered 496 Component[] labelPanel1 = ((JPanel)comps.get(2)).getComponents(); 497 Component[] labelPanel2 = ((JPanel)comps.get(1)).getComponents(); 498 if (labelPanel1[1] instanceof JPanel && labelPanel2[1] instanceof JPanel) { 499 JPanel recentPanel = (JPanel)labelPanel1[1]; 500 JPanel patternPanel = (JPanel)labelPanel2[1]; 501 recentPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Recent Files")); 502 patternPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("File Pattern")); 503 504 // Make the container panel 505 JPanel filePanel = new JPanel(); 506 507 GroupLayout layout = new GroupLayout(filePanel); 508 filePanel.setLayout(layout); 509 layout.setHorizontalGroup( 510 layout.createParallelGroup(LEADING) 511 .addGroup(layout.createSequentialGroup() 512 .addComponent(recentPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE) 513 .addGap(GAP_RELATED) 514 .addComponent(patternPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE) 515 ) 516 ); 517 layout.setVerticalGroup( 518 layout.createParallelGroup(LEADING) 519 .addGroup(layout.createSequentialGroup() 520 .addGroup(layout.createParallelGroup(TRAILING) 521 .addComponent(recentPanel, LEADING, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE) 522 .addComponent(patternPanel, LEADING, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)) 523 ) 524 ); 525 526 newComps.add(McVGuiUtils.makeLabeledComponent("Directory:", filePanel)); 527 } 528 else { 529 newComps.add(comps.get(1)); 530 newComps.add(comps.get(2)); 531 } 532 newComps.add(comps.get(3)); 533 } 534 else { 535 newComps = comps; 536 } 537 return GuiUtils.top(GuiUtils.vbox(newComps)); 538 } 539 540 /** 541 * Set the status message appropriately 542 */ 543 protected void updateStatus() { 544 super.updateStatus(); 545 String selectedReference = "the selected data"; 546 547 if(!getHaveData()) { 548 setStatus("Select zero, one, or multiple files"); 549 GuiUtils.enableTree(bottomPanel, false); 550 return; 551 } 552 553 if (isDirectory) { 554 selectedReference = "all files in this directory"; 555 } 556 else { 557 if (fileCount > 0) { 558 if (fileCount > 1) selectedReference = "the selected files"; 559 else selectedReference = "the selected file"; 560 } 561 if (directoryCount > 0) { 562 selectedReference = "the selected directory"; 563 } 564 } 565 GuiUtils.enableTree(bottomPanel, isDirectory || directoryCount > 0); 566 setStatus("Press \"" + CMD_LOAD + "\" to load " + selectedReference, "buttons"); 567 } 568 569 /** 570 * Get the top panel for the chooser 571 * @return the top panel 572 */ 573// protected JPanel getTopPanel() { 574// return McVGuiUtils.makeLabeledComponent("Source Name:", pollingInfo.getNameWidget()); 575// } 576 577 /** 578 * Get the center panel for the chooser 579 * @return the center panel 580 */ 581 protected JPanel getCenterPanel() { 582 fileChooser = doMakeDirectoryChooser(getPath()); 583 fileChooser.setPreferredSize(new Dimension(300, 300)); 584 fileChooser.setMultiSelectionEnabled(getAllowMultiple()); 585 586 fileChooser.addPropertyChangeListener( 587 JFileChooser.DIRECTORY_CHANGED_PROPERTY, 588 createPropertyListener() 589 ); 590 591 JPanel centerPanel; 592 JComponent accessory = getAccessory(); 593 if (accessory == null) { 594 centerPanel = GuiUtils.center(fileChooser); 595 } else { 596 centerPanel = GuiUtils.centerRight(fileChooser, GuiUtils.top(accessory)); 597 } 598 centerPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder()); 599 return McVGuiUtils.makeLabeledComponent("Files:", centerPanel); 600 } 601 602 /** 603 * Get the bottom panel for the chooser 604 * @return the bottom panel 605 */ 606 protected JPanel getBottomPanel() { 607 608 // Pull apart the PollingInfo components and rearrange them 609 // Don't want to override PollingInfo because it isn't something the user sees 610 // Arranged like: Label, Panel; Label, Panel; Label, Panel; etc... 611 List comps = new ArrayList(); 612 List newComps = new ArrayList(); 613 pollingInfo.getPropertyComponents(comps, false, pollingInfo.getFileCount()>0); 614 for (int i=0; i<comps.size()-1; i++) { 615 JComponent compLabel = (JComponent)comps.get(i); 616 if (compLabel instanceof JLabel) { 617 i++; 618 JComponent compPanel = (JComponent)comps.get(i); 619 if (compPanel instanceof JPanel) { 620 newComps.add(processPollingOption((JLabel)compLabel, (JPanel)compPanel)); 621 } 622 } 623 } 624 625 JPanel pollingPanel = processPollingOptions(newComps); 626 return pollingPanel; 627 } 628 629 /** 630 * Returns an {@link ActionListener} that should be listening to 631 * {@link #pollingCbx}. 632 * 633 * <p>This listener allows users to enable/disable directory watches.</p> 634 * 635 * @return {@code ActionListener} that enables/disables directory watching. 636 */ 637 private ActionListener buildPollingActionListener() { 638 return e -> { 639 logger.trace("fired: pollingCbx={}", pollingCbx.isSelected()); 640 if (!pollingCbx.isSelected()) { 641 handleStopWatchService( 642 Constants.EVENT_FILECHOOSER_STOP, 643 "disabled refreshes" 644 ); 645 } else { 646 handleStartWatchService( 647 Constants.EVENT_FILECHOOSER_START, 648 "enabled refreshes" 649 ); 650 SwingUtilities.invokeLater(this::doUpdate); 651 } 652 }; 653 } 654 655} 656