001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2023 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * http://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; 030 031import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList; 032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast; 033import static ucar.unidata.xml.XmlUtil.getAttribute; 034 035import java.awt.Insets; 036import java.awt.geom.Rectangle2D; 037 038import java.io.BufferedReader; 039import java.io.File; 040import java.io.FileOutputStream; 041import java.io.FileReader; 042import java.io.IOException; 043import java.io.PrintStream; 044 045import java.lang.reflect.Method; 046import java.net.URL; 047import java.net.URLConnection; 048import java.net.URLStreamHandler; 049 050import java.security.Security; 051import java.util.Collections; 052import java.util.Date; 053import java.util.EnumSet; 054import java.util.HashMap; 055import java.util.Hashtable; 056import java.util.LinkedList; 057import java.util.List; 058import java.util.Map; 059import java.util.Objects; 060import java.util.Properties; 061import java.util.Set; 062 063import javax.swing.Icon; 064import javax.swing.JButton; 065import javax.swing.JCheckBox; 066import javax.swing.JComponent; 067import javax.swing.JDialog; 068import javax.swing.JLabel; 069import javax.swing.JOptionPane; 070import javax.swing.SwingUtilities; 071import javax.swing.ToolTipManager; 072 073import com.formdev.flatlaf.FlatDarkLaf; 074import edu.wisc.ssec.mcidas.adde.AddeURL; 075import edu.wisc.ssec.mcidas.adde.AddeURLStreamHandler; 076 077import com.google.common.base.Strings; 078import edu.wisc.ssec.mcidasv.collaboration.CollaborationManager; 079import edu.wisc.ssec.mcidasv.ui.McIDASVXmlUi; 080import edu.wisc.ssec.mcidasv.util.McVGuiUtils; 081import edu.wisc.ssec.mcidasv.util.OptionPaneClicker; 082import edu.wisc.ssec.mcidasv.util.SystemState; 083import edu.wisc.ssec.mcidasv.util.WebBrowser; 084import edu.wisc.ssec.mcidasv.util.WelcomeWindow; 085 086import javafx.application.Platform; 087import javafx.embed.swing.JFXPanel; 088import org.joda.time.DateTime; 089import org.python.util.PythonInterpreter; 090import org.w3c.dom.Element; 091 092import ucar.nc2.NetcdfFile; 093import visad.VisADException; 094 095import ucar.unidata.data.DataManager; 096import ucar.unidata.idv.ArgsManager; 097import ucar.unidata.idv.ControlDescriptor; 098import ucar.unidata.idv.IdvObjectStore; 099import ucar.unidata.idv.IdvPersistenceManager; 100import ucar.unidata.idv.IdvPreferenceManager; 101import ucar.unidata.idv.IdvResourceManager; 102import ucar.unidata.idv.IntegratedDataViewer; 103import ucar.unidata.idv.PluginManager; 104import ucar.unidata.idv.VMManager; 105import ucar.unidata.idv.ViewDescriptor; 106import ucar.unidata.idv.ViewManager; 107import ucar.unidata.idv.chooser.IdvChooserManager; 108import ucar.unidata.idv.collab.CollabManager; 109import ucar.unidata.idv.ui.IdvUIManager; 110import ucar.unidata.idv.ui.IdvWindow; 111import ucar.unidata.ui.colortable.ColorTableManager; 112import ucar.unidata.ui.InteractiveShell.ShellHistoryEntry; 113import ucar.unidata.util.FileManager; 114import ucar.unidata.util.GuiUtils; 115import ucar.unidata.util.IOUtil; 116import ucar.unidata.util.LogUtil; 117import ucar.unidata.util.Misc; 118import ucar.unidata.xml.XmlDelegateImpl; 119import ucar.unidata.xml.XmlEncoder; 120import ucar.unidata.xml.XmlUtil; 121 122import org.bushe.swing.event.EventBus; 123import org.bushe.swing.event.annotation.AnnotationProcessor; 124import org.bushe.swing.event.annotation.EventSubscriber; 125 126import org.slf4j.bridge.SLF4JBridgeHandler; 127import org.slf4j.Logger; 128import org.slf4j.LoggerFactory; 129 130import uk.org.lidalia.sysoutslf4j.context.LogLevel; 131import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J; 132 133import edu.wisc.ssec.mcidasv.data.GpmIosp; 134import edu.wisc.ssec.mcidasv.data.TropomiIOSP; 135import edu.wisc.ssec.mcidasv.chooser.McIdasChooserManager; 136import edu.wisc.ssec.mcidasv.control.LambertAEA; 137import edu.wisc.ssec.mcidasv.data.McvDataManager; 138import edu.wisc.ssec.mcidasv.monitors.MonitorManager; 139import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource; 140import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus; 141import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType; 142import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity; 143import edu.wisc.ssec.mcidasv.servermanager.AddePreferences; 144import edu.wisc.ssec.mcidasv.servermanager.EntryStore; 145import edu.wisc.ssec.mcidasv.servermanager.EntryTransforms; 146import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry; 147import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat; 148import edu.wisc.ssec.mcidasv.servermanager.RemoteAddeEntry; 149import edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager; 150import edu.wisc.ssec.mcidasv.startupmanager.StartupManager; 151import edu.wisc.ssec.mcidasv.ui.LayerAnimationWindow; 152import edu.wisc.ssec.mcidasv.ui.McIdasColorTableManager; 153import edu.wisc.ssec.mcidasv.ui.UIManager; 154import edu.wisc.ssec.mcidasv.util.gui.EventDispatchThreadHangMonitor; 155import edu.wisc.ssec.mcidasv.util.pathwatcher.DirectoryWatchService; 156import edu.wisc.ssec.mcidasv.util.pathwatcher.OnFileChangeListener; 157import edu.wisc.ssec.mcidasv.util.pathwatcher.SimpleDirectoryWatchService; 158 159/** 160 * Class used as the {@literal "gateway"} to a running McIDAS-V session. 161 * 162 * <p>This is where the startup and shutdown processes are handled, as well as 163 * the initialization of the application's various {@literal "managers"}.</p> 164 */ 165@SuppressWarnings("unchecked") 166public class McIDASV extends IntegratedDataViewer { 167 168 /** Logging object. */ 169 private static final Logger logger = 170 LoggerFactory.getLogger(McIDASV.class); 171 172 /** 173 * Initialization start time. This value is set at the beginning of 174 * {@link #main(String[])}, and is used to estimate the duration of the 175 * session's initialization phase. 176 */ 177 private static long startTime; 178 179 /** 180 * Initialization duration. Set at the end of {@link #initDone()}. 181 */ 182 private static long estimate; 183 184 /** 185 * Path to a {@literal "session"} file--it's created upon McIDAS-V 186 * starting and removed when McIDAS-V exits cleanly. This allows us to 187 * perform a primitive check to see if the current session has happened 188 * after a crash. 189 */ 190 private static String SESSION_FILE = getSessionFilePath(); 191 192 /** 193 * Whether or not the previous session was able to exit as it should. 194 * If {@code false}, the previous session likely crashed. 195 */ 196 private static boolean cleanExit = true; 197 198 /** Date the previous session was started. May be {@code null}. */ 199 private static Date previousStart = null; 200 201 /** Set to true only if "-forceaqua" was found in the command line. */ 202 public static boolean useAquaLookAndFeel = false; 203 204 /** Points to the adde image defaults. */ 205 public static final IdvResourceManager.XmlIdvResource RSC_FRAMEDEFAULTS = 206 new IdvResourceManager.XmlIdvResource("idv.resource.framedefaults", 207 "McIDAS-X Frame Defaults"); 208 209 /** Points to the server definitions. */ 210 public static final IdvResourceManager.XmlIdvResource RSC_SERVERS = 211 new IdvResourceManager.XmlIdvResource("idv.resource.servers", 212 "Servers", "servers\\.xml$"); 213 214 /** Used to access McIDAS-V state in a static context. */ 215 private static McIDASV staticMcv; 216 217 /** Accessory in file save dialog */ 218 private JCheckBox overwriteDataCbx = 219 new JCheckBox("Change data paths", false); 220 221 /** Chooser manager */ 222 protected McIdasChooserManager chooserManager; 223 224 /** HTTP based monitor to dump stack traces and shutdown McIDAS-V. */ 225 private McIDASVMonitor mcvMonitor; 226 227 /** 228 * {@link MonitorManager} allows for relatively easy and efficient 229 * monitoring of various resources. 230 */ 231 private final MonitorManager monitorManager = new MonitorManager(); 232 233 /** 234 * Actions passed into {@link #handleAction(String, Hashtable, boolean)}. 235 */ 236 private final List<String> actions = new LinkedList<>(); 237 238 private enum WarningResult { OK, CANCEL, SHOW, HIDE }; 239 240 /** Reference to the ADDE server manager. */ 241 private EntryStore addeEntries; 242 243 /** 244 * GUI wrapper for ADDE server management. Reference is kept due to some 245 * of the trickery used by {@link AddePreferences}. 246 * 247 * <p>Value may be {@code null}.</p> 248 */ 249 private TabbedAddeManager tabbedAddeManager = null; 250 251 /** Directory monitoring service. */ 252 private final DirectoryWatchService watchService; 253 254 /** 255 * Create the McIDASV with the given command line arguments. 256 * This constructor calls {@link IntegratedDataViewer#init()} 257 * 258 * @param args Command line arguments 259 * @exception VisADException from construction of VisAd objects 260 * @exception RemoteException from construction of VisAD objects 261 */ 262 public McIDASV(String[] args) throws IOException, VisADException { 263 super(args); 264 265 AnnotationProcessor.process(this); 266 267 staticMcv = this; 268 269 // Set up our application to respond to the Mac OS X application menu 270 registerForMacOSXEvents(); 271 272 // we're tired of the IDV's default missing image, so reset it 273 GuiUtils.MISSING_IMAGE = 274 "/edu/wisc/ssec/mcidasv/resources/icons/toolbar/mcidasv-round22.png"; 275 276 watchService = new SimpleDirectoryWatchService(); 277 278 this.init(); 279 280 // ensure jython init only happens once per application session 281 String cacheDir = getStore().getJythonCacheDir(); 282 Properties pyProps = new Properties(); 283 if (cacheDir != null) { 284 pyProps.put("python.home", cacheDir); 285 } 286 String[] blank = new String[] { "" }; 287 PythonInterpreter.initialize(System.getProperties(), pyProps, blank); 288 } 289 290 /** 291 * Generic registration with the macOS application menu. 292 * 293 * <p>Checks the platform, then attempts to register with Apple's 294 * {@literal "EAWT"} stuff.</p> 295 * 296 * <p>See {@code OSXAdapter.java} to learn how this is done without 297 * directly referencing any Apple APIs.</p> 298 */ 299 public void registerForMacOSXEvents() { 300 // TODO(jon): remove? 301 if (isMac()) { 302 try { 303 // Generate and register the OSXAdapter, passing it a hash of all the methods we wish to 304 // use as delegates for various com.apple.eawt.ApplicationListener methods 305 Class<?> thisClass = getClass(); 306 Class<?>[] args = (Class[])null; 307 OSXAdapter.setQuitHandler(this, thisClass.getDeclaredMethod("MacOSXQuit", args)); 308 OSXAdapter.setAboutHandler(this, thisClass.getDeclaredMethod("MacOSXAbout", args)); 309 OSXAdapter.setPreferencesHandler(this, thisClass.getDeclaredMethod("MacOSXPreferences", args)); 310 } catch (Exception e) { 311 logger.error("Error while loading the OSXAdapter", e); 312 } 313 } 314 } 315 316 public boolean MacOSXQuit() { 317 // TODO(jon): remove? 318 return quit(); 319 } 320 321 public void MacOSXAbout() { 322 // TODO(jon): remove? 323 getIdvUIManager().about(); 324 } 325 326 public void MacOSXPreferences() { 327 // TODO(jon): remove? 328 showPreferenceManager(); 329 } 330 331 /** 332 * Get the maximum number of threads to be used when rendering in VisAD. 333 * 334 * @return Number of threads for rendering. Default value is the same as 335 * {@link Runtime#availableProcessors()}. 336 */ 337 @Override public int getMaxRenderThreadCount() { 338 StateManager stateManager = (StateManager)getStateManager(); 339 return stateManager.getPropertyOrPreference(PREF_THREADS_RENDER, 340 Runtime.getRuntime().availableProcessors()); 341 } 342 343 /** 344 * Get the maximum number of threads to be used when reading data. 345 * 346 * @return Number of threads for reading data. Default value is {@code 4}. 347 */ 348 @Override public int getMaxDataThreadCount() { 349 StateManager stateManager = (StateManager)getStateManager(); 350 return stateManager.getPropertyOrPreference(PREF_THREADS_DATA, 4); 351 } 352 353 /** 354 * Start up the McIDAS-V monitor server. 355 * 356 * <p>This is an HTTP server on the port defined by the property 357 * {@code idv.monitorport}. Default value is 8788.</p> 358 * 359 * <p>It is only accessible to 127.0.0.1 (localhost).</p> 360 */ 361 @Override protected void startMonitor() { 362 if (mcvMonitor != null) { 363 return; 364 } 365 final String monitorPort = getProperty(PROP_MONITORPORT, ""); 366 if (monitorPort!=null && monitorPort.trim().length()>0 && !"none".equals(monitorPort.trim())) { 367 Misc.run(() -> { 368 try { 369 mcvMonitor = 370 new McIDASVMonitor(McIDASV.this, 371 Integer.parseInt(monitorPort)); 372 mcvMonitor.init(); 373 } catch (Exception exc) { 374 LogUtil.consoleMessage("Unable to start McIDAS-V monitor on port:" + monitorPort); 375 LogUtil.consoleMessage("Error:" + exc); 376 } 377 }); 378 } 379 } 380 381 /** 382 * Initializes a XML encoder with McIDAS-V specific XML delegates. 383 * 384 * @param encoder XML encoder that'll be dealing with persistence. 385 * @param forRead Not used as of yet. 386 */ 387 // TODO: if we ever get up past three or so XML delegates, I vote that we 388 // make our own version of VisADPersistence. 389 @Override protected void initEncoder(XmlEncoder encoder, boolean forRead) { 390 391 encoder.addDelegateForClass(LambertAEA.class, new XmlDelegateImpl() { 392 @Override public Element createElement(XmlEncoder e, Object o) { 393 LambertAEA projection = (LambertAEA)o; 394 Rectangle2D rect = projection.getDefaultMapArea(); 395 List args = Misc.newList(rect); 396 List types = Misc.newList(rect.getClass()); 397 return e.createObjectConstructorElement(o, args, types); 398 } 399 }); 400 401 encoder.addDelegateForClass(ShellHistoryEntry.class, new XmlDelegateImpl() { 402 @Override public Element createElement(XmlEncoder e, Object o) { 403 ShellHistoryEntry entry = (ShellHistoryEntry)o; 404 List args = Misc.newList(entry.getEntryBytes(), entry.getInputMode().toString()); 405 return e.createObjectConstructorElement(o, args); 406 } 407 }); 408 409 // TODO(jon): ultra fashion makeover!! 410 encoder.addDelegateForClass(RemoteAddeEntry.class, new XmlDelegateImpl() { 411 @Override public Element createElement(XmlEncoder e, Object o) { 412 RemoteAddeEntry entry = (RemoteAddeEntry)o; 413 Element element = e.createObjectElement(o.getClass()); 414 element.setAttribute("address", entry.getAddress()); 415 element.setAttribute("group", entry.getGroup()); 416 element.setAttribute("username", entry.getAccount().getUsername()); 417 element.setAttribute("project", entry.getAccount().getProject()); 418 element.setAttribute("source", entry.getEntrySource().toString()); 419 element.setAttribute("type", entry.getEntryType().toString()); 420 element.setAttribute("validity", entry.getEntryValidity().toString()); 421 element.setAttribute("status", entry.getEntryStatus().toString()); 422 element.setAttribute("temporary", Boolean.toString(entry.isEntryTemporary())); 423 element.setAttribute("alias", entry.getEntryAlias()); 424 return element; 425 } 426 427 @Override public Object createObject(XmlEncoder e, Element element) { 428 String address = getAttribute(element, "address"); 429 String group = getAttribute(element, "group"); 430 String username = getAttribute(element, "username"); 431 String project = getAttribute(element, "project"); 432 String source = getAttribute(element, "source"); 433 String type = getAttribute(element, "type"); 434 String validity = getAttribute(element, "validity"); 435 String status = getAttribute(element, "status"); 436 boolean temporary = getAttribute(element, "temporary", false); 437 String alias = getAttribute(element, "alias", ""); 438 439 EntrySource entrySource = EntryTransforms.strToEntrySource(source); 440 EntryType entryType = EntryTransforms.strToEntryType(type); 441 EntryValidity entryValidity = EntryTransforms.strToEntryValidity(validity); 442 EntryStatus entryStatus = EntryTransforms.strToEntryStatus(status); 443 444 return new RemoteAddeEntry.Builder(address, group) 445 .account(username, project) 446 .source(entrySource) 447 .type(entryType) 448 .validity(entryValidity) 449 .temporary(temporary) 450 .alias(alias) 451 .status(entryStatus).build(); 452 } 453 }); 454 455 encoder.addDelegateForClass(LocalAddeEntry.class, new XmlDelegateImpl() { 456 @Override public Element createElement(XmlEncoder e, Object o) { 457 LocalAddeEntry entry = (LocalAddeEntry)o; 458 Element element = e.createObjectElement(o.getClass()); 459 element.setAttribute("group", entry.getGroup()); 460 element.setAttribute("descriptor", entry.getDescriptor()); 461 element.setAttribute("realtime", entry.getRealtimeAsString()); 462 element.setAttribute("format", entry.getFormat().name()); 463 element.setAttribute("start", entry.getStart()); 464 element.setAttribute("end", entry.getEnd()); 465 element.setAttribute("fileMask", entry.getMask()); 466 element.setAttribute("name", entry.getName()); 467 element.setAttribute("status", entry.getEntryStatus().name()); 468 element.setAttribute("temporary", Boolean.toString(entry.isEntryTemporary())); 469 element.setAttribute("alias", entry.getEntryAlias()); 470 return element; 471 } 472 473 @Override public Object createObject(XmlEncoder e, Element element) { 474 String group = getAttribute(element, "group"); 475 String descriptor = getAttribute(element, "descriptor"); 476 String realtime = getAttribute(element, "realtime", ""); 477 AddeFormat format = EntryTransforms.strToAddeFormat(XmlUtil.getAttribute(element, "format")); 478 String start = getAttribute(element, "start", "1"); 479 String end = getAttribute(element, "end", "999999"); 480 String fileMask = getAttribute(element, "fileMask"); 481 String name = getAttribute(element, "name"); 482 String status = getAttribute(element, "status", "ENABLED"); 483 boolean temporary = getAttribute(element, "temporary", false); 484 String alias = getAttribute(element, "alias", ""); 485 486 return new LocalAddeEntry.Builder(name, group, fileMask, format) 487 .range(start, end) 488 .descriptor(descriptor) 489 .realtime(realtime) 490 .status(status) 491 .temporary(temporary) 492 .alias(alias).build(); 493 } 494 }); 495 496 // Move legacy classes to a new location 497 encoder.registerNewClassName( 498 "edu.wisc.ssec.mcidasv.data.Test2ImageDataSource", 499 "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource"); 500 encoder.registerNewClassName( 501 "edu.wisc.ssec.mcidasv.data.Test2AddeImageDataSource", 502 "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource"); 503 encoder.registerNewClassName( 504 "edu.wisc.ssec.mcidasv.data.AddePointDataSource", 505 "edu.wisc.ssec.mcidasv.data.adde.AddePointDataSource"); 506 encoder.registerNewClassName( 507 "edu.wisc.ssec.mcidasv.data.AddeSoundingAdapter", 508 "edu.wisc.ssec.mcidasv.data.adde.AddeSoundingAdapter"); 509 } 510 511 /** 512 * Returns <i>all</i> of the actions used in this McIDAS-V session. This is 513 * possibly TMI and might be removed... 514 * 515 * @return Actions executed thus far. 516 */ 517 public List<String> getActionHistory() { 518 return actions; 519 } 520 521 /** 522 * Converts {@link ArgsManager#getOriginalArgs()} to a {@link List} and 523 * returns. 524 * 525 * @return The command-line arguments used to start McIDAS-V, as an 526 * {@code ArrayList}. 527 */ 528 public List<String> getCommandLineArgs() { 529 String[] originalArgs = getArgsManager().getOriginalArgs(); 530 List<String> args = arrList(originalArgs.length); 531 Collections.addAll(args, originalArgs); 532 return args; 533 } 534 535 /** 536 * Captures the action passed to {@code handleAction}. The action is logged 537 * and additionally, if the action is a HTML link, we attempt to visit the 538 * link in the user's preferred browser. 539 */ 540 @Override public boolean handleAction(String action, Hashtable properties, 541 boolean checkForAlias) 542 { 543 actions.add(action); 544 545 boolean result = false; 546 DateTime start = DateTime.now(); 547 logger.trace("started: action='{}', checkForAlias={}, properties='{}'", action, checkForAlias, properties); 548 if (IOUtil.isHtmlFile(action)) { 549 WebBrowser.browse(action); 550 result = true; 551 } else { 552 if (action.toLowerCase().contains("showsupportform")) { 553 logger.trace("showing support form 'manually'..."); 554 getIdvUIManager().showSupportForm(); 555 result = true; 556 } else { 557 result = super.handleAction(action, properties, checkForAlias); 558 } 559 } 560 long duration = new DateTime().minus(start.getMillis()).getMillis(); 561 logger.trace("finished: action='{}', duration: {} (ms), checkForAlias={}, properties='{}'", action, duration, checkForAlias, properties); 562 563 return result; 564 } 565 566 /** 567 * This method checks if the given action is one of the following. 568 * <ul> 569 * <li>Jython code: starts with {@literal "jython:"}.</li> 570 * <li>Help link: starts with {@literal "help:"}.</li> 571 * <li>Resource bundle file: ends with {@literal ".rbi"}.</li> 572 * <li>Bundle file: ends with {@literal ".xidv"}.</li> 573 * <li>JNLP file: ends with {@literal ".jnlp"}.</li> 574 * </ul> 575 * 576 * <p>It returns {@code true} if the action is one of these. {@code false} 577 * otherwise.</p> 578 * 579 * @param action The string action 580 * @param properties any properties 581 * 582 * @return {@code true} if the action was {@literal "handled"}; 583 * {@code false} otherwise. 584 */ 585 @Override protected boolean handleFileOrUrlAction(String action, 586 Hashtable properties) { 587 boolean result = false; 588 boolean idvAction = action.startsWith("idv:"); 589 boolean jythonAction = action.startsWith("jython:"); 590 591 if (!idvAction && !jythonAction) { 592 return super.handleFileOrUrlAction(action, properties); 593 } 594 595 Map<String, Object> hashProps; 596 if (properties != null) { 597 hashProps = new HashMap<>(properties); 598 } else { 599 //noinspection CollectionWithoutInitialCapacity 600 hashProps = new HashMap<>(); 601 } 602 603 ucar.unidata.idv.JythonManager jyManager = getJythonManager(); 604 if (idvAction) { 605 action = action.replace("&", "&").substring(4); 606 jyManager.evaluateUntrusted(action, hashProps); 607 result = true; 608 } else if (jythonAction) { 609 action = action.substring(7); 610 jyManager.evaluateAction(action, hashProps); 611 result = true; 612 } else { 613 result = super.handleFileOrUrlAction(action, properties); 614 } 615 return result; 616 } 617 618 /** 619 * Add a new {@link ControlDescriptor} into the {@code controlDescriptor} 620 * list and {@code controlDescriptorMap}. 621 * 622 * <p>This method differs from the IDV's in that McIDAS-V <b>overwrites</b> 623 * existing {@code ControlDescriptor ControlDescriptors} if 624 * {@link ControlDescriptor#getControlId()} matches. 625 * 626 * @param cd The ControlDescriptor to add. 627 * 628 * @throws NullPointerException if {@code cd} is {@code null}. 629 */ 630 @Override protected void addControlDescriptor(ControlDescriptor cd) { 631 cd = Objects.requireNonNull(cd, "Cannot add a null control descriptor to the list of control descriptors."); 632 String id = cd.getControlId(); 633 if (controlDescriptorMap.get(id) == null) { 634 controlDescriptors.add(cd); 635 controlDescriptorMap.put(id, cd); 636 } else { 637 for (int i = 0; i < controlDescriptors.size(); i++) { 638 ControlDescriptor tmp = (ControlDescriptor)controlDescriptors.get(i); 639 if (tmp.getControlId().equals(id)) { 640 controlDescriptors.set(i, cd); 641 controlDescriptorMap.put(id, cd); 642 break; 643 } 644 } 645 } 646 } 647 648 /** 649 * Get the {@code Hashtable} that maps {@link ControlDescriptor} IDs to the 650 * {@code ControlDescriptor} instance. 651 * 652 * <p>Mostly used as a convenient way to check if a given 653 * {@code ControlDescriptor} has already been processed.</p> 654 * 655 * @return {@code Hashtable} that maps {@code String} IDs to the 656 * corresponding {@code ControlDescriptor}. 657 */ 658 public Hashtable<String, ControlDescriptor> getControlDescriptorMap() { 659 return controlDescriptorMap; 660 } 661 662 // pop up an incredibly rudimentary window that controls layer viz animation. 663 public void showLayerVisibilityAnimator() { 664 logger.trace("probably should try to do something here."); 665 SwingUtilities.invokeLater(() -> { 666 try { 667 LayerAnimationWindow window = new LayerAnimationWindow(); 668 window.setVisible(true); 669 } catch (Exception e) { 670 logger.error("oh no! something happened!", e); 671 } 672 }); 673 } 674 675 /** 676 * Handles removing all loaded data sources. 677 * 678 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 679 * will ignore the user's preferences and remove all data sources. 680 * 681 * @param showWarning Whether or not to display a warning message before 682 * removing <i>all</i> data sources. See the return details for more. 683 * 684 * @return Either {@code true} if the user wants to continue showing the 685 * warning dialog, or {@code false} if they've elected to stop showing the 686 * warning. If {@code showWarning} is {@code false}, this method will 687 * always return {@code false}, as the user isn't interested in seeing the 688 * warning. 689 */ 690 public boolean removeAllData(final boolean showWarning) { 691 boolean reallyRemove = false; 692 boolean continueWarning = true; 693 694 if (getArgsManager().getIsOffScreen()) { 695 super.removeAllDataSources(); 696 return continueWarning; 697 } 698 699 if (showWarning) { 700 Set<WarningResult> result = showWarningDialog( 701 "Confirm Data Removal", 702 "This action will remove all of the data currently loaded in McIDAS-V.<br>Is this what you want to do?", 703 Constants.PREF_CONFIRM_REMOVE_DATA, 704 "Always ask?", 705 "Remove all data", 706 "Do not remove any data"); 707 reallyRemove = result.contains(WarningResult.OK); 708 continueWarning = result.contains(WarningResult.SHOW); 709 } else { 710 // user doesn't want to see warning messages. 711 reallyRemove = true; 712 continueWarning = false; 713 } 714 715 if (reallyRemove) { 716 super.removeAllDataSources(); 717 } 718 719 return continueWarning; 720 } 721 722 /** 723 * Handles removing all loaded layers ({@literal "displays"} in IDV-land). 724 * 725 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 726 * will ignore the user's preferences and remove all layers. 727 * 728 * @param showWarning Whether or not to display a warning message before 729 * removing <i>all</i> layers. See the return details for more. 730 * 731 * @return Either {@code true} if the user wants to continue showing the 732 * warning dialog, or {@code false} if they've elected to stop showing the 733 * warning. If {@code showWarning} is {@code false}, this method will 734 * always return {@code false}, as the user isn't interested in seeing the 735 * warning. 736 */ 737 public boolean removeAllLayers(final boolean showWarning) { 738 boolean reallyRemove = false; 739 boolean continueWarning = true; 740 741 if (getArgsManager().getIsOffScreen()) { 742 super.removeAllDisplays(); 743 ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations(); 744 return continueWarning; 745 } 746 747 if (showWarning) { 748 Set<WarningResult> result = showWarningDialog( 749 "Confirm Layer Removal", 750 "This action will remove every layer currently loaded in McIDAS-V.<br>Is this what you want to do?", 751 Constants.PREF_CONFIRM_REMOVE_LAYERS, 752 "Always ask?", 753 "Remove all layers", 754 "Do not remove any layers"); 755 reallyRemove = result.contains(WarningResult.OK); 756 continueWarning = result.contains(WarningResult.SHOW); 757 } else { 758 // user doesn't want to see warning messages. 759 reallyRemove = true; 760 continueWarning = false; 761 } 762 763 if (reallyRemove) { 764 super.removeAllDisplays(); 765 ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations(); 766 } 767 768 return continueWarning; 769 } 770 771 /** 772 * Overridden so that McIDAS-V can prompt the user before removing, if 773 * necessary. 774 */ 775 @Override public void removeAllDataSources() { 776 IdvObjectStore store = getStore(); 777 boolean showWarning = 778 store.get(Constants.PREF_CONFIRM_REMOVE_DATA, true); 779 showWarning = removeAllData(showWarning); 780 store.put(Constants.PREF_CONFIRM_REMOVE_DATA, showWarning); 781 } 782 783 /** 784 * Overridden so that McIDAS-V can prompt the user before removing, if 785 * necessary. 786 */ 787 @Override public void removeAllDisplays() { 788 IdvObjectStore store = getStore(); 789 boolean showWarning = 790 store.get(Constants.PREF_CONFIRM_REMOVE_LAYERS, true); 791 showWarning = removeAllLayers(showWarning); 792 store.put(Constants.PREF_CONFIRM_REMOVE_LAYERS, showWarning); 793 } 794 795 /** 796 * Handles removing all loaded layers ({@literal "displays"} in IDV-land) 797 * and data sources. 798 * 799 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 800 * will ignore the user's preferences and remove all layers and data. 801 * 802 * @see #removeAllData(boolean) 803 * @see #removeAllLayers(boolean) 804 */ 805 public void removeAllLayersAndData() { 806 boolean reallyRemove = false; 807 boolean continueWarning = true; 808 809 if (getArgsManager().getIsOffScreen()) { 810 removeAllData(false); 811 removeAllLayers(false); 812 } 813 814 IdvObjectStore store = getStore(); 815 boolean showWarning = store.get(Constants.PREF_CONFIRM_REMOVE_BOTH, true); 816 if (showWarning) { 817 Set<WarningResult> result = showWarningDialog( 818 "Confirm Removal", 819 "This action will remove all of your currently loaded layers and data.<br>Is this what you want to do?", 820 Constants.PREF_CONFIRM_REMOVE_BOTH, 821 "Always ask?", 822 "Remove all layers and data", 823 "Do not remove anything"); 824 reallyRemove = result.contains(WarningResult.OK); 825 continueWarning = result.contains(WarningResult.SHOW); 826 } else { 827 // user doesn't want to see warning messages. 828 reallyRemove = true; 829 continueWarning = false; 830 } 831 832 // don't show the individual warning messages as the user has attempted 833 // to remove *both* 834 if (reallyRemove) { 835 removeAllData(false); 836 removeAllLayers(false); 837 } 838 839 store.put(Constants.PREF_CONFIRM_REMOVE_BOTH, continueWarning); 840 } 841 842 /** 843 * Helper method for showing the removal warning dialog. Note that none of 844 * these parameters should be {@code null} or empty. 845 * 846 * @param title Title of the warning dialog. 847 * @param message Contents of the warning. May contain HTML, but you do 848 * not need to provide opening and closing {@literal "html"} tags. 849 * @param prefId ID of the preference that controls whether or not the 850 * dialog should be displayed. 851 * @param prefLabel Brief description of the preference. 852 * @param okLabel Text of button that signals removal. 853 * @param cancelLabel Text of button that signals cancelling removal. 854 * 855 * @return A {@code Set} of {@link WarningResult WarningResults} that 856 * describes what the user opted to do. Should always contain only 857 * <b>two</b> elements. One for whether or not {@literal "ok"} or 858 * {@literal "cancel"} was clicked, and one for whether or not the warning 859 * should continue to be displayed. 860 */ 861 private Set<WarningResult> showWarningDialog(final String title, 862 final String message, final String prefId, final String prefLabel, 863 final String okLabel, final String cancelLabel) 864 { 865 JCheckBox box = new JCheckBox(prefLabel, true); 866 JComponent comp = GuiUtils.vbox( 867 new JLabel("<html>"+message+"</html>"), 868 GuiUtils.inset(box, new Insets(4, 15, 0, 10))); 869 870 Object[] options = { okLabel, cancelLabel }; 871 int result = JOptionPane.showOptionDialog( 872 LogUtil.getCurrentWindow(), // parent 873 comp, // msg 874 title, // title 875 JOptionPane.YES_NO_OPTION, // option type 876 JOptionPane.WARNING_MESSAGE, // message type 877 (Icon)null, // icon? 878 options, // selection values 879 options[1]); // initial? 880 881 WarningResult button = WarningResult.CANCEL; 882 if (result == JOptionPane.YES_OPTION) { 883 button = WarningResult.OK; 884 } 885 886 WarningResult show = WarningResult.HIDE; 887 if (box.isSelected()) { 888 show = WarningResult.SHOW; 889 } 890 891 return EnumSet.of(button, show); 892 } 893 894 public void removeTabData() { 895 } 896 897 public void removeTabLayers() { 898 899 } 900 901 public void removeTabLayersAndData() { 902 } 903 904 /** 905 * Overridden so that McIDAS-V doesn't have to create an entire new 906 * {@link ucar.unidata.idv.ui.IdvWindow} if 907 * {@link VMManager#findViewManager(ViewDescriptor)} can't find an 908 * appropriate ViewManager for {@code viewDescriptor}. 909 * 910 * <p>Not doing the above causes McIDAS-V to get stuck in a window creation 911 * loop.</p> 912 */ 913 @Override public ViewManager getViewManager(ViewDescriptor viewDescriptor, 914 boolean newWindow, String properties) 915 { 916 ViewManager vm = 917 getVMManager().findOrCreateViewManager(viewDescriptor, properties); 918 if (vm == null) { 919 vm = super.getViewManager(viewDescriptor, newWindow, properties); 920 } 921 return vm; 922 } 923 924 /** 925 * Returns a reference to the current McIDAS-V object. Useful for working 926 * inside static methods. <b>Always check for null when using this 927 * method</b>. 928 * 929 * @return Either the current McIDAS-V "god object" or {@code null}. 930 */ 931 public static McIDASV getStaticMcv() { 932 return staticMcv; 933 } 934 935 /** 936 * @see ucar.unidata.idv.IdvBase#setIdv(ucar.unidata.idv.IntegratedDataViewer) 937 */ 938 @Override public void setIdv(IntegratedDataViewer idv) { 939 this.idv = idv; 940 } 941 942 /** 943 * Load the McV properties. All other property files are disregarded. 944 * 945 * @see ucar.unidata.idv.IntegratedDataViewer#initPropertyFiles(java.util.List) 946 */ 947 @Override public void initPropertyFiles(List files) { 948 files.clear(); 949 files.add(Constants.PROPERTIES_FILE); 950 } 951 952 /** 953 * Makes {@link PersistenceManager} save off a default {@literal "layout"} 954 * bundle. 955 */ 956 public void doSaveAsDefaultLayout() { 957 Misc.run(() -> ((PersistenceManager)getPersistenceManager()).doSaveAsDefaultLayout()); 958 } 959 960 /** 961 * Determines whether or not a default layout exists. 962 * 963 * @return {@code true} if there is a default layout, {@code false} 964 * otherwise. 965 */ 966 public boolean hasDefaultLayout() { 967 String path = 968 getResourceManager().getResources(IdvResourceManager.RSC_BUNDLES) 969 .getWritable(); 970 return new File(path).exists(); 971 } 972 973 /** 974 * Called from the menu command to clear the default bundle. Overridden 975 * in McIDAS-V so that we reference the <i>layout</i> rather than the 976 * bundle. 977 */ 978 @Override public void doClearDefaults() { 979 if (GuiUtils.showYesNoDialog(null, 980 "Are you sure you want to delete your default layout?", 981 "Delete confirmation")) { 982 resourceManager.clearDefaultBundles(); 983 } 984 } 985 986 /** 987 * Returns the time it took for McIDAS-V to start up. 988 * 989 * @return These results are created from subtracting the results of two 990 * {@link System#nanoTime()} calls against one another. 991 */ 992 public long getStartupDuration() { 993 return estimate; 994 } 995 996 /** 997 * <p> 998 * Overridden so that the support form becomes non-modal if launched from 999 * an exception dialog. 1000 * </p> 1001 * 1002 * @see IntegratedDataViewer#addErrorButtons(JDialog, List, String, Throwable) 1003 */ 1004 @Override public void addErrorButtons(final JDialog dialog, 1005 List buttonList, final String msg, final Throwable exc) 1006 { 1007 JButton supportBtn = new JButton("Support Form"); 1008 supportBtn.addActionListener(ae -> 1009 getIdvUIManager().showSupportForm(msg, 1010 LogUtil.getStackTrace(exc), 1011 null)); 1012 buttonList.add(supportBtn); 1013 } 1014 1015 /** 1016 * This method is useful for storing commandline {@literal "properties"} 1017 * with the user's preferences. 1018 */ 1019 private void overridePreferences() { 1020 StateManager stateManager = (StateManager)getStateManager(); 1021 int renderThreads = getMaxRenderThreadCount(); 1022 stateManager.putPreference(PREF_THREADS_RENDER, renderThreads); 1023 stateManager.putPreference(PREF_THREADS_DATA, getMaxDataThreadCount()); 1024 visad.util.ThreadManager.setGlobalMaxThreads(renderThreads); 1025 } 1026 1027 /** 1028 * Determine if the last {@literal "exit"} was clean--whether or not 1029 * {@code SESSION_FILE} was removed before the McIDAS-V process terminated. 1030 * 1031 * <p>If the exit was not clean, the user is prompted to submit a support 1032 * request.</p> 1033 */ 1034 private void detectAndHandleCrash() { 1035 GuiUtils.setApplicationTitle(""); 1036 if (cleanExit || getArgsManager().getIsOffScreen()) { 1037 return; 1038 } 1039 1040 String msg = "The previous McIDAS-V session did not exit cleanly.<br>"+ 1041 "Do you want to send the log file to the McIDAS Help Desk?"; 1042 if (previousStart != null) { 1043 msg = "The previous McIDAS-V session (start time: %s) did not exit cleanly.<br>"+ 1044 "Do you want to send the log file to the McIDAS Help Desk?"; 1045 msg = String.format(msg, previousStart); 1046 } 1047 1048 boolean continueAsking = getStore().get("mcv.crash.boom.send.report", true); 1049 if (!continueAsking) { 1050 return; 1051 } 1052 1053 Set<WarningResult> result = showWarningDialog( 1054 "Report Crash", 1055 msg, 1056 "mcv.crash.boom.send.report", 1057 "Always ask?", 1058 "Open support form", 1059 "Do not report"); 1060 1061 getStore().put("mcv.crash.boom.send.report", result.contains(WarningResult.SHOW)); 1062 if (!result.contains(WarningResult.OK)) { 1063 return; 1064 } 1065 1066 getIdvUIManager().showSupportForm(); 1067 } 1068 1069 /** 1070 * Called after the IDV has finished setting everything up after starting. 1071 * McIDAS-V is currently only using this method to determine if the last 1072 * {@literal "exit"} was clean--whether or not {@code SESSION_FILE} was 1073 * removed before the McIDAS-V process terminated. 1074 * 1075 * Called after the IDV has finished setting everything up. McIDAS-V uses 1076 * this method to handle: 1077 * 1078 * <ul> 1079 * <li>Clearing out the automatic display creation arguments.</li> 1080 * <li>Presence of certain properties on the commandline.</li> 1081 * <li>Detection and handling of a crashed McIDAS-V session.</li> 1082 * <li>Run action specified by {@code -doaction} flag (if any).</li> 1083 * <li>Allowing tooltips to remain visible for more than 4 seconds.</li> 1084 * </ul> 1085 * 1086 * @see ArgumentManager#clearAutomaticDisplayArgs() 1087 * @see #overridePreferences() 1088 * @see #detectAndHandleCrash() 1089 */ 1090 @Override public void initDone() { 1091 ((ArgumentManager)argsManager).clearAutomaticDisplayArgs(); 1092 1093 overridePreferences(); 1094 1095 detectAndHandleCrash(); 1096 1097 if (addeEntries == null) { 1098 getServerManager(); 1099 } 1100 addeEntries.startLocalServer(); 1101 1102 estimate = System.nanoTime() - startTime; 1103 logger.info("estimated startup duration: {} ms", estimate / 1.0e6); 1104 System.setProperty("mcv.start.duration", Long.toString(estimate)); 1105 1106 // handle the -doAction <action id> startup option. 1107 ((ArgumentManager)getArgsManager()).runStartupAction(); 1108 1109 // disable idiotic tooltip dismissal (seriously, 4 seconds!?) 1110 ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE); 1111 1112 // turn on directory monitoring in the file choosers. 1113 startWatchService(); 1114 EventBus.publish(Constants.EVENT_FILECHOOSER_START, "init finished"); 1115 1116 EventDispatchThreadHangMonitor.initMonitoring(); 1117 } 1118 1119 /** 1120 * @see IntegratedDataViewer#doOpen(String, boolean, boolean) 1121 */ 1122 @Override public void doOpen(final String filename, 1123 final boolean checkUserPreference, final boolean andRemove) 1124 { 1125 doOpenInThread(filename, checkUserPreference, andRemove); 1126 } 1127 1128 /** 1129 * Have the user select a bundle. If andRemove is true then we remove all 1130 * data sources and displays. 1131 * 1132 * Then we open the bundle and start doing unpersistence things. 1133 * 1134 * @param filename The filename to open 1135 * @param checkUserPreference Should we show, if needed, the 1136 * {@literal "open"} dialog 1137 * @param andRemove If true then first remove all data sources and displays 1138 */ 1139 private void doOpenInThread(String filename, boolean checkUserPreference, 1140 boolean andRemove) 1141 { 1142 boolean overwriteData = false; 1143 if (filename == null) { 1144 if (overwriteDataCbx.getToolTipText() == null) { 1145 overwriteDataCbx.setToolTipText("Change the file paths that the data sources use"); 1146 } 1147 1148 filename = FileManager.getReadFile("Open File", 1149 ((ArgumentManager)getArgsManager()).getBundleFilters(true), 1150 GuiUtils.top(overwriteDataCbx)); 1151 1152 if (filename == null) { 1153 return; 1154 } 1155 1156 overwriteData = overwriteDataCbx.isSelected(); 1157 } 1158 1159 if (ArgumentManager.isXmlBundle(filename)) { 1160 getPersistenceManager().decodeXmlFile(filename, 1161 checkUserPreference, overwriteData); 1162 return; 1163 } 1164 handleAction(filename, null); 1165 } 1166 1167 /** 1168 * Factory method to create the McIDAS-V @link JythonManager}. 1169 * 1170 * @return New {@code JythonManager}. 1171 */ 1172 @Override protected JythonManager doMakeJythonManager() { 1173 logger.debug("returning a new JythonManager"); 1174 return new JythonManager(this); 1175 } 1176 1177 /** 1178 * Factory method to create a McIDAS-V {@link CollaborationManager}. 1179 * 1180 * @return New {@code CollaborationManager}. 1181 */ 1182 @Override protected CollabManager doMakeCollabManager() { 1183 return new CollaborationManager(this); 1184 } 1185 1186 /** 1187 * Factory method to create a McIDAS-V {@link McIdasChooserManager}. 1188 * Here we create our own manager so it can do things specific to McIDAS-V. 1189 * 1190 * @return {@code McIdasChooserManager} indicated by the startup properties. 1191 * 1192 * @see ucar.unidata.idv.IdvBase#doMakeIdvChooserManager() 1193 */ 1194 @Override 1195 protected IdvChooserManager doMakeIdvChooserManager() { 1196 chooserManager = (McIdasChooserManager)makeManager( 1197 McIdasChooserManager.class, new Object[] { this }); 1198 chooserManager.init(); 1199 return chooserManager; 1200 } 1201 1202 /** 1203 * Factory method to create the {@link IdvUIManager}. Here we create our 1204 * own UI manager so it can do things specific to McIDAS-V. 1205 * 1206 * @return {@link UIManager} indicated by the startup properties. 1207 * 1208 * @see ucar.unidata.idv.IdvBase#doMakeIdvUIManager() 1209 */ 1210 @Override 1211 protected IdvUIManager doMakeIdvUIManager() { 1212 return new UIManager(this); 1213 } 1214 1215 /** 1216 * Create our own VMManager so that we can make the tabs play nice. 1217 * @see ucar.unidata.idv.IdvBase#doMakeVMManager() 1218 */ 1219 @Override 1220 protected VMManager doMakeVMManager() { 1221 // what an ugly class name :( 1222 return new ViewManagerManager(this); 1223 } 1224 1225 /** 1226 * Make the {@link McIdasPreferenceManager}. 1227 * @see ucar.unidata.idv.IdvBase#doMakePreferenceManager() 1228 */ 1229 @Override 1230 protected IdvPreferenceManager doMakePreferenceManager() { 1231 return new McIdasPreferenceManager(this); 1232 } 1233 1234 /** 1235 * <p>McIDAS-V (alpha 10+) needs to handle both IDV bundles without 1236 * component groups and all bundles from prior McV alphas. You better 1237 * believe we need to extend the persistence manager functionality!</p> 1238 * 1239 * @see ucar.unidata.idv.IdvBase#doMakePersistenceManager() 1240 */ 1241 @Override protected IdvPersistenceManager doMakePersistenceManager() { 1242 return new PersistenceManager(this); 1243 } 1244 1245 /** 1246 * Create, if needed, and return the {@link McIdasChooserManager}. 1247 * 1248 * @return The Chooser manager 1249 */ 1250 public McIdasChooserManager getMcIdasChooserManager() { 1251 return (McIdasChooserManager)getIdvChooserManager(); 1252 } 1253 1254 /** 1255 * Returns the {@link MonitorManager}. 1256 * 1257 * @return McIDAS-V {@literal "monitor manager"}. 1258 */ 1259 public MonitorManager getMonitorManager() { 1260 return monitorManager; 1261 } 1262 1263 /** 1264 * Responds to events generated by the server manager's GUI. Currently 1265 * limited to {@link edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager.Event#CLOSED TabbedAddeManager.Event#CLOSED}. 1266 * 1267 * @param evt {@code TabbedAddeManager} event to respond to. 1268 */ 1269 @EventSubscriber(eventClass=TabbedAddeManager.Event.class) 1270 public void onServerManagerWindowEvent(TabbedAddeManager.Event evt) { 1271 if (evt == TabbedAddeManager.Event.CLOSED) { 1272 tabbedAddeManager = null; 1273 } 1274 } 1275 1276 /** 1277 * Creates (if needed) the server manager GUI and displays it. 1278 */ 1279 public void showServerManager() { 1280 if (tabbedAddeManager == null) { 1281 tabbedAddeManager = new TabbedAddeManager(getServerManager()); 1282 } 1283 tabbedAddeManager.showManager(); 1284 } 1285 1286 /** 1287 * Creates a new server manager (if needed) and returns it. 1288 * 1289 * @return The McIDAS-V ADDE server manager. 1290 */ 1291 public EntryStore getServerManager() { 1292 if (addeEntries == null) { 1293 addeEntries = new EntryStore(getStore(), getResourceManager()); 1294 } 1295 return addeEntries; 1296 } 1297 1298 public McvDataManager getMcvDataManager() { 1299 return (McvDataManager)getDataManager(); 1300 } 1301 1302 /** 1303 * Get McIDASV. 1304 * @see ucar.unidata.idv.IdvBase#getIdv() 1305 */ 1306 @Override public IntegratedDataViewer getIdv() { 1307 return this; 1308 } 1309 1310 /** 1311 * Creates a McIDAS-V argument manager so that McV can handle some non-IDV 1312 * command line things. 1313 * 1314 * @param args The arguments from the command line. 1315 * 1316 * @see ucar.unidata.idv.IdvBase#doMakeArgsManager(java.lang.String[]) 1317 */ 1318 @Override protected ArgsManager doMakeArgsManager(String[] args) { 1319 return new ArgumentManager(this, args); 1320 } 1321 1322 /** 1323 * Factory method to create the {@link McvDataManager}. 1324 * 1325 * @return The data manager 1326 * 1327 * @see ucar.unidata.idv.IdvBase#doMakeDataManager() 1328 */ 1329 @Override protected DataManager doMakeDataManager() { 1330 return new McvDataManager(this); 1331 } 1332 1333 /** 1334 * Make the McIDAS-V {@link StateManager}. 1335 * @see ucar.unidata.idv.IdvBase#doMakeStateManager() 1336 */ 1337 @Override protected StateManager doMakeStateManager() { 1338 return new StateManager(this); 1339 } 1340 1341 /** 1342 * Make the McIDAS-V {@link ResourceManager}. 1343 * @see ucar.unidata.idv.IdvBase#doMakeResourceManager() 1344 */ 1345 @Override protected IdvResourceManager doMakeResourceManager() { 1346 return new ResourceManager(this); 1347 } 1348 1349 /** 1350 * Make the {@link McIdasColorTableManager}. 1351 * @see ucar.unidata.idv.IdvBase#doMakeColorTableManager() 1352 */ 1353 @Override protected ColorTableManager doMakeColorTableManager() { 1354 return new McIdasColorTableManager(); 1355 } 1356 1357 /** 1358 * Factory method to create the {@link McvPluginManager}. 1359 * 1360 * @return The McV plugin manager. 1361 * 1362 * @see ucar.unidata.idv.IdvBase#doMakePluginManager() 1363 */ 1364 @Override protected PluginManager doMakePluginManager() { 1365 return new McvPluginManager(this); 1366 } 1367 1368// /** 1369// * Make the {@link edu.wisc.ssec.mcidasv.data.McIDASVProjectionManager}. 1370// * @see ucar.unidata.idv.IdvBase#doMakeIdvProjectionManager() 1371// */ 1372// @Override 1373// protected IdvProjectionManager doMakeIdvProjectionManager() { 1374// return new McIDASVProjectionManager(this); 1375// } 1376 1377 /** 1378 * Make a help button for a particular help topic 1379 * 1380 * @param helpId the topic id 1381 * @param toolTip the tooltip 1382 * 1383 * @return the button 1384 */ 1385 @Override public JComponent makeHelpButton(String helpId, String toolTip) { 1386 JButton btn = McVGuiUtils.makeImageButton(Constants.ICON_HELP, 1387 getIdvUIManager(), "showHelp", helpId, "Show help"); 1388 1389 if (toolTip != null) { 1390 btn.setToolTipText(toolTip); 1391 } 1392 return btn; 1393 } 1394 1395 /** 1396 * Return the current {@literal "userpath"}. 1397 * 1398 * @return Path to the user's {@literal "McIDAS-V directory"}. 1399 */ 1400 public String getUserDirectory() { 1401 return StartupManager.getInstance().getPlatform().getUserDirectory(); 1402 } 1403 1404 /** 1405 * Return the path to a file within {@literal "userpath"}. 1406 * 1407 * @param filename File within the userpath. 1408 * 1409 * @return Path to a file within the user's {@literal "McIDAS-V directory"}. 1410 * No path validation is performed, so please be aware that the returned 1411 * path may not exist. 1412 */ 1413 public String getUserFile(String filename) { 1414 return StartupManager.getInstance().getPlatform().getUserFile(filename); 1415 } 1416 1417 /** 1418 * Invokes the main method for a given class. 1419 * 1420 * <p>Note: this is rather limited so far as it doesn't pass in any 1421 * arguments.</p> 1422 * 1423 * @param className Class whose main method is to be invoked. Cannot be 1424 * {@code null}. 1425 */ 1426 public void runPluginMainMethod(final String className) { 1427 final String[] dummyArgs = { "" }; 1428 try { 1429 Class<?> clazz = Misc.findClass(className); 1430 Class[] args = new Class[] { dummyArgs.getClass() }; 1431 Method mainMethod = Misc.findMethod(clazz, "main", args); 1432 if (mainMethod != null) { 1433 mainMethod.invoke(null, new Object[] { dummyArgs }); 1434 } 1435 } catch (Exception e) { 1436 logger.error("problem with plugin class", e); 1437 LogUtil.logException("problem running main method for class: "+className, e); 1438 } 1439 } 1440 1441 /** 1442 * Attempts to determine if a given string is a 1443 * {@literal "loopback address"} (aka localhost). 1444 * 1445 * <p>Strings are <b>trimmed and converted to lowercase</b>, and currently 1446 * checked against: 1447 * <ul> 1448 * <li>{@code 127.0.0.1}</li> 1449 * <li>{@code ::1} (for IPv6)</li> 1450 * <li>Strings starting with {@code localhost}.</li> 1451 * </ul> 1452 * 1453 * @param host {@code String} to check. Should not be {@code null}. 1454 * 1455 * @return {@code true} if {@code host} is a recognized loopback address. 1456 * {@code false} otherwise. 1457 * 1458 * @throws NullPointerException if {@code host} is {@code null}. 1459 */ 1460 public static boolean isLoopback(final String host) { 1461 String cleaned = Objects.requireNonNull(host.trim().toLowerCase()); 1462 return "127.0.0.1".startsWith(cleaned) 1463 || "::1".startsWith(cleaned) 1464 || cleaned.startsWith("localhost"); 1465 } 1466 1467 /** 1468 * Are we on a Mac? Used to build the MRJ handlers, taken from TN2110. 1469 * 1470 * @return {@code true} if this session is running on top of OS X, 1471 * {@code false} otherwise. 1472 * 1473 * @see <a href="http://developer.apple.com/technotes/tn2002/tn2110.html">TN2110</a> 1474 */ 1475 public static boolean isMac() { 1476 String osName = System.getProperty("os.name"); 1477 return osName.contains("OS X"); 1478 } 1479 1480 /** 1481 * Queries the {@code os.name} system property and if the result does not 1482 * start with {@literal "Windows"}, the platform is assumed to be 1483 * {@literal "unix-like"}. 1484 * 1485 * <p>Given the McIDAS-V supported platforms (Windows, {@literal "Unix"}, 1486 * and OS X), the above logic is safe. 1487 * 1488 * @return {@code true} if we're not running on Windows, {@code false} 1489 * otherwise. 1490 * 1491 * @throws RuntimeException if there is no property associated with 1492 * {@code os.name}. 1493 */ 1494 public static boolean isUnixLike() { 1495 String osName = System.getProperty("os.name"); 1496 if (osName == null) { 1497 throw new RuntimeException("no os.name system property!"); 1498 } 1499 1500 if (System.getProperty("os.name").startsWith("Windows")) { 1501 return false; 1502 } 1503 return true; 1504 } 1505 1506 /** 1507 * Queries the {@code os.name} system property and if the result starts 1508 * with {@literal "Windows"}, the platform is assumed to be Windows. Duh. 1509 * 1510 * @return {@code true} if we're running on Windows, {@code false} 1511 * otherwise. 1512 * 1513 * @throws RuntimeException if there is no property associated with 1514 * {@code os.name}. 1515 */ 1516 public static boolean isWindows() { 1517 String osName = System.getProperty("os.name"); 1518 if (osName == null) { 1519 throw new RuntimeException("no os.name system property!"); 1520 } 1521 1522 return osName.startsWith("Windows"); 1523 } 1524 1525 /** 1526 * If McIDAS-V is running on Windows, this method will return a 1527 * {@code String} that looks like {@literal "C:"} or {@literal "D:"}, etc. 1528 * 1529 * <p>If McIDAS-V is not running on Windows, this method will return an 1530 * empty {@code String}. 1531 * 1532 * @return Either the {@literal "drive letter"} of the {@code java.home} 1533 * property or an empty {@code String} if McIDAS-V isn't running on Windows. 1534 * 1535 * @throws RuntimeException if there is no property associated with 1536 * {@code java.home}. 1537 */ 1538 public static String getJavaDriveLetter() { 1539 if (!isWindows()) { 1540 return ""; 1541 } 1542 1543 String home = System.getProperty("java.home"); 1544 if (home == null) { 1545 throw new RuntimeException("no java.home system property!"); 1546 } 1547 1548 return home.substring(0, 2); 1549 } 1550 1551 /** 1552 * Attempts to create a {@literal "session"} file. This method will create 1553 * a {@literal "userpath"} if it does not already exist. 1554 * 1555 * @param path Path of the session file that should get created. 1556 * {@code null} values are not allowed, and sufficient priviledges are 1557 * assumed. 1558 * 1559 * @throws AssertionError if McIDAS-V couldn't write to {@code path}. 1560 * 1561 * @see #SESSION_FILE 1562 * @see #hadCleanExit(String) 1563 * @see #removeSessionFile(String) 1564 */ 1565 private static void createSessionFile(final String path) { 1566 assert path != null : "Cannot create a null path"; 1567 try ( 1568 FileOutputStream out = new FileOutputStream(path); 1569 PrintStream p = new PrintStream(out) 1570 ) { 1571 p.println(System.currentTimeMillis()); 1572 } catch (Exception e) { 1573 throw new AssertionError("Could not write to "+path+". Error message: "+e.getMessage(), e); 1574 } 1575 } 1576 1577 /** 1578 * Attempts to extract a timestamp from {@code path}. {@code path} is 1579 * expected to <b>only</b> contain a single line consisting of a 1580 * {@link Long} integer. 1581 * 1582 * @param path Path to the file of interest. 1583 * 1584 * @return Either a {@link Date} of the timestamp contained in 1585 * {@code path} or {@code null} if the extraction failed. 1586 */ 1587 private static Date extractDate(final String path) { 1588 assert path != null; 1589 Date savedDate = null; 1590 try (BufferedReader r = new BufferedReader(new FileReader(path))) { 1591 String line = r.readLine(); 1592 if (line != null) { 1593 savedDate = new Date(Long.parseLong(line.trim())); 1594 } 1595 } catch (Exception e) { 1596 logger.warn("Could not extract date from session file", e); 1597 } 1598 return savedDate; 1599 } 1600 1601 /** 1602 * Attempts to remove the file accessible via {@code path}. 1603 * 1604 * @param path Path of the file that'll get removed. This should be 1605 * non-null and point to an existing and writable filename (not a 1606 * directory). 1607 * 1608 * @throws AssertionError if the file at {@code path} could not be 1609 * removed. 1610 * 1611 * @see #SESSION_FILE 1612 * @see #createSessionFile(String) 1613 * @see #hadCleanExit(String) 1614 */ 1615 private static void removeSessionFile(final String path) { 1616 if (path == null) { 1617 return; 1618 } 1619 1620 File f = new File(path); 1621 1622 if (!f.exists() || !f.canWrite() || f.isDirectory()) { 1623 return; 1624 } 1625 1626 if (!f.delete()) { 1627 throw new AssertionError("Could not delete session file"); 1628 } 1629 } 1630 1631 /** 1632 * Tries to determine whether or not the last McIDAS-V session ended 1633 * {@literal "cleanly"}. Currently a simple check for a 1634 * {@literal "session"} file that is created upon starting and removed upon 1635 * ending. 1636 * 1637 * @param path Path to the session file to check. Can't be {@code null}. 1638 * 1639 * @return Either {@code true} if the file pointed at by {@code path} does 1640 * <b><i>NOT</i></b> exist, {@code false} if it does exist. 1641 * 1642 * @see #SESSION_FILE 1643 * @see #createSessionFile(String) 1644 * @see #removeSessionFile(String) 1645 */ 1646 private static boolean hadCleanExit(final String path) { 1647 assert path != null : "Cannot test for a null path"; 1648 return !(new File(path).exists()); 1649 } 1650 1651 /** 1652 * Returns the (<i>current</i>) path to the session file. Note that the 1653 * location of the file may change arbitrarily. 1654 * 1655 * @return {@code String} pointing to the session file. 1656 * 1657 * @see #SESSION_FILE 1658 */ 1659 public static String getSessionFilePath() { 1660 return StartupManager.getInstance().getPlatform().getUserFile("session.tmp"); 1661 } 1662 1663 /** 1664 * Useful for providing the startup manager with values other than the 1665 * defaults... Note that this method attempts to update the value of 1666 * {@link #SESSION_FILE}. 1667 * 1668 * @param args Likely the argument array coming from the main method. 1669 */ 1670 private static void applyArgs(final String[] args) { 1671 assert args != null : "Cannot use a null argument array"; 1672 StartupManager.applyArgs(true, false, args); 1673 SESSION_FILE = getSessionFilePath(); 1674 } 1675 1676 /** 1677 * This returns the set of {@link ControlDescriptor ControlDescriptors} 1678 * that can be shown. The ordering of this list determines the 1679 * "default" controls shown in the Field Selector, so we override 1680 * here for control over the ordering. 1681 * 1682 * @param includeTemplates If true then include the display templates 1683 * @return re-ordered List of shown control descriptors 1684 */ 1685 @Override public List getControlDescriptors(boolean includeTemplates) { 1686 List<ControlDescriptor> l = 1687 cast(super.getControlDescriptors(includeTemplates)); 1688 for (int i = 0; i < l.size(); i++) { 1689 ControlDescriptor cd = l.get(i); 1690 if (cd.getControlId().equals("omni")) { 1691 // move the omni control to the end of the list 1692 // so it will never be "default" in Field Selector 1693 l.remove(i); 1694 l.add(cd); 1695 } 1696 } 1697 return l; 1698 } 1699 1700 /** 1701 * Show the McIDAS-V {@literal "Welcome Window"} for the first start up. 1702 * 1703 * @param args Commandline arguments, used to handle autoquit stress testing. 1704 */ 1705 private static void handleWelcomeWindow(String... args) { 1706 boolean showWelcome = false; 1707 boolean welcomeAutoQuit = false; 1708 long welcomeAutoQuitDelay = WelcomeWindow.DEFAULT_QUIT_DELAY; 1709 for (int i = 0; i < args.length; i++) { 1710 if ("-welcomewindow".equals(args[i])) { 1711 showWelcome = true; 1712 } else if ("-autoquit".equals(args[i])) { 1713 welcomeAutoQuit = true; 1714 int delayIdx = i + 1; 1715 if (delayIdx < args.length) { 1716 welcomeAutoQuitDelay = Long.parseLong(args[delayIdx]); 1717 } 1718 } 1719 } 1720 1721 // if user elects to quit, System.exit(1) will be called. 1722 // if the user decides to start, the welcome window will be simply be 1723 // closed, allowing McV to continue starting up. 1724 if (showWelcome) { 1725 WelcomeWindow ww; 1726 if (welcomeAutoQuit) { 1727 ww = new WelcomeWindow(true, welcomeAutoQuitDelay); 1728 } else { 1729 ww = new WelcomeWindow(); 1730 } 1731 ww.setVisible(true); 1732 } 1733 } 1734 1735 /** 1736 * Register {@literal "adde"} and {@literal "idvresource"} URL protocols. 1737 * 1738 * <p>This needs to be called pretty early in the McIDAS-V initialization 1739 * process. They're currently being registered immediately after the 1740 * session file is created.</p> 1741 */ 1742 private static void registerProtocolHandlers() { 1743 try { 1744 URL.setURLStreamHandlerFactory(protocol -> { 1745 switch (protocol.toLowerCase()) { 1746 case AddeURL.ADDE_PROTOCOL: 1747 return new AddeURLStreamHandler(); 1748 case PluginManager.PLUGIN_PROTOCOL: 1749 return new IdvResourceStreamHandler(); 1750 default: 1751 return null; 1752 } 1753 }); 1754 } catch (Throwable e) { 1755 logger.error("Could not register protocol handlers!", e); 1756 } 1757 } 1758 1759 /** 1760 * Responsible for handling {@literal "idvresource"} URLs. 1761 * 1762 * <p>Really just a redirect to {@link IOUtil#getURL(String, Class)}.</p> 1763 */ 1764 private static class IdvResourceStreamHandler extends URLStreamHandler { 1765 @Override protected URLConnection openConnection(URL u) 1766 throws IOException 1767 { 1768 return IOUtil.getURL(u.getPath(), McIDASV.class).openConnection(); 1769 } 1770 } 1771 1772 /** 1773 * Check the value of the {@code mcidasv.darkmode} system property. 1774 * 1775 * @return {@code true} if dark mode should be enabled, {@code false} otherwise. 1776 */ 1777 public static boolean isDarkMode() { 1778 return Boolean.parseBoolean(System.getProperty("mcidasv.darkmode", "false")); 1779 } 1780 1781 /** 1782 * Configure the logging and create the McIDAS-V object responsible for 1783 * initializing the application session. 1784 * 1785 * @param args Command line arguments. 1786 * 1787 * @throws Exception When something untoward happens. 1788 */ 1789 public static void main(String... args) throws Exception { 1790 // show the welcome window if needed. 1791 // since the welcome window is intended to be a one time thing, 1792 // it probably shouldn't count for the startTime stuff. 1793 handleWelcomeWindow(args); 1794 1795 startTime = System.nanoTime(); 1796 1797 // allow use of the "unlimited strength" crypto policy. 1798 // this becomes the default in 1.8.0_162, but we need to ship with 1799 // 1.8.0_152. 1800 Security.setProperty("crypto.policy", "unlimited"); 1801 1802 // Configure GUI look and feel before any UI stuff 1803 if (isDarkMode()) { 1804 FlatDarkLaf.setup(); 1805 } 1806 1807 // the following two lines are required if we want to embed JavaFX 1808 // widgets into McV (which is Swing). The first line initializes the 1809 // JavaFX runtime, and the second line allows the JavaFX runtime to 1810 // hang around even if there are no JavaFX windows. 1811 JFXPanel dummy = new JFXPanel(); 1812 Platform.setImplicitExit(false); 1813 1814 try { 1815 applyArgs(args); 1816 1817 SysOutOverSLF4J.sendSystemOutAndErrToSLF4J(LogLevel.INFO, LogLevel.WARN); 1818 1819 // Optionally remove existing handlers attached to j.u.l root logger 1820 SLF4JBridgeHandler.removeHandlersForRootLogger(); // (since SLF4J 1.6.5) 1821 1822 // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during 1823 // the initialization phase of your application 1824 SLF4JBridgeHandler.install(); 1825 1826// Properties pythonProps = new Properties(); 1827// logger.trace("calling PythonInterpreter.initialize..."); 1828// PythonInterpreter.initialize(System.getProperties(), pythonProps, new String[] {""}); 1829 1830 LogUtil.configure(); 1831 1832 // Register IOSPs here 1833 1834 // TJJ Jan 2019 - new IOSP for TropOMI data 1835 NetcdfFile.registerIOProvider(TropomiIOSP.class); 1836 // Experimental - this was 2016ish for GPM data, should still work 1837 NetcdfFile.registerIOProvider(GpmIosp.class); 1838 1839 long sysMem = Long.valueOf(SystemState.queryOpSysProps().get("opsys.memory.physical.total")); 1840 logger.info("============================================================================="); 1841 logger.info("Starting McIDAS-V @ {}", new Date()); 1842 logger.info("Versions:"); 1843 logger.info("{}", SystemState.getMcvVersionString()); 1844 logger.info("{}", SystemState.getIdvVersionString()); 1845 logger.info("{}", SystemState.getVisadVersionString()); 1846 logger.info("{}", SystemState.getNcidvVersionString()); 1847 logger.info("{} MB system memory", Math.round(sysMem/1024/1024)); 1848 1849 if (!hadCleanExit(SESSION_FILE)) { 1850 previousStart = extractDate(SESSION_FILE); 1851 } 1852 1853 createSessionFile(SESSION_FILE); 1854 registerProtocolHandlers(); 1855 1856 McIDASV myself = new McIDASV(args); 1857 } catch (IllegalArgumentException e) { 1858 String msg = "McIDAS-V could not initialize itself. "; 1859 String osName = System.getProperty("os.name"); 1860 if (osName.contains("Windows")) { 1861 LogUtil.userErrorMessage(msg+'\n'+e.getMessage()); 1862 } else { 1863 System.err.println(msg+e.getMessage()); 1864 } 1865 } 1866 } 1867 1868 /** 1869 * Log (TRACE level) the time spent in various discrete parts of 1870 * McIDAS-V's startup process. 1871 * 1872 * <p>Currently tracking time spent: 1873 * <ul> 1874 * <li>creating GUI components with {@link McIDASVXmlUi}</li> 1875 * <li>initialing Jython interpreters.</li> 1876 * </ul></p> 1877 * 1878 * <p><b>The elapsed times are merely a quick estimate.</b> The only way 1879 * to obtain accurate timing information with the JVM is using 1880 * <a href="https://openjdk.java.net/projects/code-tools/jmh/">JMH</a>.</p> 1881 * 1882 * @see McIDASVXmlUi#getElapsedGuiTime() 1883 * @see JythonManager#getElapsedInterpreterInit() 1884 */ 1885 public static void elapsedStartup() { 1886 long totalXmlUi = 0L; 1887 List<IdvWindow> windows = cast(IdvWindow.getWindows()); 1888 for (IdvWindow window : windows) { 1889 McIDASVXmlUi xmlUi = (McIDASVXmlUi)window.getXmlUI(); 1890 if (xmlUi != null) { 1891 long winElapsed = xmlUi.getElapsedGuiTime(); 1892 totalXmlUi += winElapsed; 1893 logger.trace("title '{}' xmlui '{}' spent {} ms creating components", 1894 window.getTitle(), 1895 Integer.toHexString(xmlUi.hashCode()), 1896 String.format("%.2f", winElapsed / 1.0e6)); 1897 } else { 1898 logger.trace("no xmlui for '{}'", window.getTitle()); 1899 } 1900 } 1901 1902 McIDASV m = getStaticMcv(); 1903 long elapsedJython = m.getJythonManager().getElapsedInterpreterInit(); 1904 1905 String xmlui = String.format("%6.2f", totalXmlUi / 1.0e6); 1906 String jython = String.format("%6.2f", elapsedJython / 1.0e6); 1907 logger.trace("estimated time spent creating xmlui GUI components : {} ms", Strings.padStart(xmlui, 9, ' ')); 1908 logger.trace("estimated time spent initializing jython interpreters: {} ms", Strings.padStart(jython, 9, ' ')); 1909 } 1910 1911 /** 1912 * Attempts a clean shutdown of McIDAS-V. Currently this entails 1913 * suppressing any error dialogs, explicitly killing the 1914 * {@link #addeEntries}, removing {@link #SESSION_FILE}, and disabling 1915 * the directory monitors found in the file choosers. 1916 * 1917 * @param exitCode System exit code to use. 1918 * 1919 * @see IntegratedDataViewer#quit() 1920 */ 1921 @Override protected void exit(int exitCode) { 1922 LogUtil.setShowErrorsInGui(false); 1923 1924 // turn off the directory monitors in the file choosers. 1925 EventBus.publish(Constants.EVENT_FILECHOOSER_STOP, "shutting down"); 1926 stopWatchService(); 1927 1928 if (addeEntries != null) { 1929 addeEntries.saveForShutdown(); 1930 addeEntries.stopLocalServer(); 1931 } 1932 1933 removeSessionFile(SESSION_FILE); 1934 1935 // shut down javafx runtime 1936 Platform.exit(); 1937 1938 // dump some information about how much time was spent doing certain 1939 // things during startup. 1940 elapsedStartup(); 1941 1942 logger.info("Exiting McIDAS-V @ {}", new Date()); 1943 1944 System.exit(exitCode); 1945 } 1946 1947 /** 1948 * This method is largely a copy of {@link IntegratedDataViewer#quit()}, 1949 * but allows for some GUI testing. 1950 */ 1951 public boolean autoQuit() { 1952 IdvObjectStore store = getStore(); 1953 1954 boolean showQuitConfirm = store.get(PREF_SHOWQUITCONFIRM, true); 1955 long quitDelay = store.get("mcidasv.autoexit.delay", 3000); 1956 1957 if (showQuitConfirm) { 1958 JCheckBox cbx = new JCheckBox("Always ask", true); 1959 JComponent comp = 1960 GuiUtils.vbox( 1961 new JLabel("<html><b>Do you really want to exit?</b></html>"), 1962 GuiUtils.inset(cbx, new Insets(4, 15, 0, 10))); 1963 1964 JOptionPane pane = new JOptionPane(comp, 1965 JOptionPane.QUESTION_MESSAGE, 1966 JOptionPane.YES_NO_OPTION); 1967 1968 new OptionPaneClicker(pane, "Exit Confirmation", quitDelay, "Yes"); 1969 getStore().put(PREF_SHOWQUITCONFIRM, cbx.isSelected()); 1970 } 1971 1972 if (!getStationModelManager().checkCloseWindow()) { 1973 return false; 1974 } 1975 1976 if (!getJythonManager().saveOnExit()) { 1977 return false; 1978 } 1979 1980 store.saveIfNeeded(); 1981 store.cleanupTmpFiles(); 1982 getPluginManager().handleApplicationExit(); 1983 getJythonManager().handleApplicationExit(); 1984 1985 if (getInteractiveMode()) { 1986 exit(0); 1987 } 1988 return true; 1989 } 1990 1991 /** 1992 * Register the given {@code listener} so that changes to files matching 1993 * {@code glob} in {@code path} can be handled. 1994 * 1995 * @param path Directory to watch. 1996 * @param glob Only respond to files matching this file mask. 1997 * @param listener Listener that can handle file changes. 1998 * 1999 * @throws IOException if there was a problem registering {@code listener}. 2000 */ 2001 public void watchDirectory(final String path, 2002 final String glob, 2003 final OnFileChangeListener listener) 2004 throws IOException 2005 { 2006 watchService.register(listener, path, glob); 2007 } 2008 2009 /** 2010 * Returns McIDAS-V's {@link DirectoryWatchService}. 2011 * 2012 * @return {@code DirectoryWatchService} responsible for handling all of 2013 * McIDAS-V's directory monitoring. 2014 */ 2015 public DirectoryWatchService getWatchService() { 2016 return watchService; 2017 } 2018 2019 /** 2020 * Enable directory monitoring. 2021 */ 2022 public void startWatchService() { 2023 watchService.start(); 2024 } 2025 2026 /** 2027 * Disable directory monitoring. 2028 */ 2029 public void stopWatchService() { 2030 if (watchService != null) { 2031 watchService.stop(); 2032 } 2033 } 2034 2035 /** 2036 * Exposes {@link #exit(int)} to other classes. 2037 * 2038 * @param exitCode System exit code to use. 2039 * 2040 * @see #exit(int) 2041 */ 2042 public void exitMcIDASV(int exitCode) { 2043 exit(exitCode); 2044 } 2045}