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("&", "&amp;").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}