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