001package apps;
002
003import apps.gui3.tabbedpreferences.TabbedPreferences;
004import apps.gui3.tabbedpreferences.TabbedPreferencesAction;
005import apps.plaf.macosx.Application;
006import apps.util.Log4JUtil;
007
008import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
009
010import java.awt.*;
011import java.awt.event.*;
012import java.beans.PropertyChangeEvent;
013import java.beans.PropertyChangeListener;
014import java.io.*;
015import java.lang.reflect.InvocationTargetException;
016import java.net.*;
017import java.util.*;
018import javax.swing.*;
019import javax.swing.text.DefaultEditorKit;
020import javax.swing.text.JTextComponent;
021
022import jmri.*;
023import jmri.jmrit.jython.*;
024import jmri.jmrit.logixng.LogixNG_Manager;
025import jmri.jmrit.logixng.LogixNGPreferences;
026import jmri.jmrit.revhistory.FileHistory;
027import jmri.jmrit.throttle.ThrottleFrame;
028import jmri.jmrix.*;
029import jmri.profile.*;
030import jmri.script.JmriScriptEngineManager;
031import jmri.util.*;
032import jmri.util.iharder.dnd.URIDrop;
033import jmri.util.prefs.JmriPreferencesActionFactory;
034import jmri.util.swing.*;
035
036/**
037 * Base class for JMRI applications.
038 *
039 * @author Bob Jacobsen Copyright 2003, 2007, 2008, 2010
040 * @author Dennis Miller Copyright 2005
041 * @author Giorgio Terdina Copyright 2008
042 * @author Matthew Harris Copyright (C) 2011
043 */
044public class Apps extends JPanel implements PropertyChangeListener, WindowListener {
045
046    static String profileFilename;
047    private Action prefsAction;  // defer initialization until needed so that Bundle accesses translate
048
049    @SuppressFBWarnings(value = {"ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", "SC_START_IN_CTOR"},
050            justification = "only one application at a time. The thread is only called to help improve user experiance when opening the preferences, it is not critical for it to be run at this stage")
051    public Apps() {
052
053        super(true);
054        long start = System.nanoTime();
055        log.trace("starting ctor at {}", start);
056
057        splash(false);
058        splash(true, true);
059        log.trace("splash screens up, about to setButtonSpace");
060        setButtonSpace();
061        log.trace("about to setJynstrumentSpace");
062        setJynstrumentSpace();
063
064        log.trace("setLogo");
065        jmri.Application.setLogo(logo());
066        log.trace("setURL");
067        jmri.Application.setURL(line2());
068
069        // Get configuration profile
070        log.trace("start to get configuration profile - locate files");
071        // Needs to be done before loading a ConfigManager or UserPreferencesManager
072        FileUtil.createDirectory(FileUtil.getPreferencesPath());
073        // Needs to be declared final as we might need to
074        // refer to this on the Swing thread
075        final File profileFile;
076        profileFilename = configFilename.replaceFirst(".xml", ".properties");
077        // decide whether name is absolute or relative
078        if (!new File(profileFilename).isAbsolute()) {
079            // must be relative, but we want it to
080            // be relative to the preferences directory
081            profileFile = new File(FileUtil.getPreferencesPath() + profileFilename);
082        } else {
083            profileFile = new File(profileFilename);
084        }
085        ProfileManager.getDefault().setConfigFile(profileFile);
086        // See if the profile to use has been specified on the command line as
087        // a system property org.jmri.profile as a profile id.
088        if (System.getProperties().containsKey(ProfileManager.SYSTEM_PROPERTY)) {
089            ProfileManager.getDefault().setActiveProfile(System.getProperty(ProfileManager.SYSTEM_PROPERTY));
090        }
091        log.trace("check if profile exists");
092        // @see jmri.profile.ProfileManager#migrateToProfiles Javadoc for conditions handled here
093        if (!profileFile.exists()) { // no profile config for this app
094            log.trace("profileFile {} doesn't exist", profileFile);
095            try {
096                if (ProfileManager.getDefault().migrateToProfiles(configFilename)) { // migration or first use
097                    // notify user of change only if migration occurred
098                    // TODO: a real migration message
099                    JmriJOptionPane.showMessageDialog(sp,
100                            Bundle.getMessage("ConfigMigratedToProfile"),
101                            jmri.Application.getApplicationName(),
102                            JmriJOptionPane.INFORMATION_MESSAGE);
103                }
104            } catch (IOException | IllegalArgumentException ex) {
105                JmriJOptionPane.showMessageDialog(sp,
106                        ex.getLocalizedMessage(),
107                        jmri.Application.getApplicationName(),
108                        JmriJOptionPane.ERROR_MESSAGE);
109                log.error("Exception migrating configuration to profiles: {}",ex.getMessage());
110            }
111        }
112        log.trace("about to try getStartingProfile");
113        try {
114            ProfileManagerDialog.getStartingProfile(sp);
115            // Manually setting the configFilename property since calling
116            // Apps.setConfigFilename() does not reset the system property
117            configFilename = FileUtil.getProfilePath() + Profile.CONFIG_FILENAME;
118            System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME);
119            Profile profile = ProfileManager.getDefault().getActiveProfile();
120            if (profile != null) {
121                log.info("Starting with profile {}", profile.getId());
122            } else {
123                log.info("Starting without a profile");
124            }
125
126            // rapid language set; must follow up later with full setting as part of preferences
127            jmri.util.gui.GuiLafPreferencesManager.setLocaleMinimally(profile);
128        } catch (IOException ex) {
129            log.info("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage());
130        }
131
132        // install a Preferences Action Factory.
133        InstanceManager.store(new AppsPreferencesActionFactory(), JmriPreferencesActionFactory.class);
134
135        // Install configuration manager and Swing error handler
136        // Constructing the AppsConfigurationManager also loads various configuration services
137        ConfigureManager cm = InstanceManager.setDefault(ConfigureManager.class, new AppsConfigurationManager());
138
139        // record startup
140        String appString = String.format("%s (v%s)", jmri.Application.getApplicationName(), Version.getCanonicalVersion());
141        InstanceManager.getDefault(FileHistory.class).addOperation("app", appString, null);
142
143        // Install abstractActionModel
144        InstanceManager.store(new apps.CreateButtonModel(), apps.CreateButtonModel.class);
145
146        // find preference file and set location in configuration manager
147        // Needs to be declared final as we might need to
148        // refer to this on the Swing thread
149        final File file;
150        File singleConfig;
151        File sharedConfig = null;
152        // decide whether name is absolute or relative
153        if (!new File(configFilename).isAbsolute()) {
154            // must be relative, but we want it to
155            // be relative to the preferences directory
156            singleConfig = new File(FileUtil.getUserFilesPath() + configFilename);
157        } else {
158            singleConfig = new File(configFilename);
159        }
160        try {
161            // get preferences file
162            sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG);
163            if (!sharedConfig.canRead()) {
164                sharedConfig = null;
165            }
166        } catch (FileNotFoundException ex) {
167            // ignore - sharedConfig will remain null in this case
168        }
169        // load config file if it exists
170        if (sharedConfig != null) {
171            file = sharedConfig;
172        } else {
173            file = singleConfig;
174        }
175
176        // ensure the UserPreferencesManager has loaded. Done on GUI
177        // thread as it can modify GUI objects
178        log.debug("*** About to getDefault(jmri.UserPreferencesManager.class) with file {}", file);
179        ThreadingUtil.runOnGUI(() -> {
180            InstanceManager.getDefault(jmri.UserPreferencesManager.class);
181        });
182        log.debug("*** Done");
183
184        // now (attempt to) load the config file
185        log.debug("Using config file(s) {}", file.getPath());
186        if (file.exists()) {
187            log.debug("start load config file {}", file.getPath());
188            try {
189                configOK = cm.load(file, true);
190            } catch (JmriException e) {
191                log.error("Unhandled problem loading configuration", e);
192                configOK = false;
193            }
194            log.debug("end load config file, OK={}", configOK);
195        } else {
196            log.info("No saved preferences, will open preferences window.  Searched for {}", file.getPath());
197            configOK = false;
198        }
199
200        // populate GUI
201        log.debug("Start UI");
202        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
203
204        // done
205        long end = System.nanoTime();
206
207        long elapsedTime = (end - start) / 1000000;
208        /*
209         This ensures that the message is displayed on the screen for a minimum of 2.5seconds, if the time taken
210         to get to this point in the code is longer that 2.5seconds then the wait is not invoked.
211         */
212        long sleep = 2500 - elapsedTime;
213        if (sleep > 0) {
214            log.debug("Debug message was displayed for less than 2500ms ({}ms). Sleeping for {}ms to allow user sufficient time to do something.",
215                    elapsedTime, sleep);
216            try {
217                Thread.sleep(sleep);
218            } catch (InterruptedException e) {
219                log.error("uexpected ", e);
220            }
221        }
222
223        FileUtil.logFilePaths();
224
225        splash(false);
226        splash(true, false);
227        Toolkit.getDefaultToolkit().removeAWTEventListener(debugListener);
228        while (debugmsg) {
229            /*The user has pressed the interupt key that allows them to disable logixs
230             at start up we do not want to process any more information until the user
231             has answered the question */
232            try {
233                Thread.sleep(1000);
234            } catch (InterruptedException e) {
235                log.error("Unexpected:",e);
236            }
237        }
238        // Now load deferred config items
239        if (file.exists()) {
240            if (file.equals(singleConfig)) {
241                // To avoid possible locks, deferred load should be
242                // performed on the Swing thread
243                if (SwingUtilities.isEventDispatchThread()) {
244                    configDeferredLoadOK = doDeferredLoad(file);
245                } else {
246                    try {
247                        // Use invokeAndWait method as we don't want to
248                        // return until deferred load is completed
249                        SwingUtilities.invokeAndWait(new Runnable() {
250                            @Override
251                            @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "configDeferredLoadOK write is semi-global")
252                            public void run() {
253                                configDeferredLoadOK = doDeferredLoad(file);
254                            }
255                        });
256                    } catch (InterruptedException | InvocationTargetException ex) {
257                        log.error("Exception creating system console frame", ex);
258                    }
259                }
260            } else {
261                // deferred loading is not done in the new config
262                configDeferredLoadOK = true;
263            }
264        } else {
265            configDeferredLoadOK = false;
266        }
267        // If preferences need to be migrated, do it now
268        if (sharedConfig == null && configOK == true && configDeferredLoadOK == true) {
269            log.info("Migrating preferences to new format...");
270            // migrate preferences
271            InstanceManager.getOptionalDefault(TabbedPreferences.class).ifPresent(tp -> {
272                // tp.init();
273                tp.saveContents();
274                cm.storePrefs();
275            });
276            // notify user of change
277            log.info("Preferences have been migrated to new format.");
278            log.info("New preferences format will be used after JMRI is restarted.");
279            if (!GraphicsEnvironment.isHeadless()) {
280                Profile profile = ProfileManager.getDefault().getActiveProfile();
281                JmriJOptionPane.showMessageDialog(sp,
282                        Bundle.getMessage("SingleConfigMigratedToSharedConfig", profile),
283                        jmri.Application.getApplicationName(),
284                        JmriJOptionPane.INFORMATION_MESSAGE);
285            }
286        }
287
288        // Before starting to load preferences, make sure some managers are created.
289        // This is needed because these aren't particularly well-behaved during
290        // creation.
291        InstanceManager.getDefault(jmri.LogixManager.class);
292        InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class);
293
294        // preload script engines if requested
295        if (Boolean.getBoolean("org.jmri.python.preload")) {
296            new Thread(() -> {
297                try {
298                    JmriScriptEngineManager.getDefault().initializeAllEngines();
299                } catch (RuntimeException ex) {
300                    log.error("Error in trying to initialize script interpreters {}", ex.getMessage());
301                }
302            }, "initialize python interpreter").start();
303        }
304
305        // kick off update of decoder index if needed
306        jmri.util.ThreadingUtil.runOnGUI(() -> {
307            try {
308                jmri.jmrit.decoderdefn.DecoderIndexFile.updateIndexIfNeeded();
309            } catch (org.jdom2.JDOMException| java.io.IOException e) {
310                log.error("Exception trying to pre-load decoderIndex", e);
311            }
312        });
313
314        // if the configuration didn't complete OK, pop the prefs frame and help
315        log.debug("Config OK? {}, deferred config OK? {}", configOK, configDeferredLoadOK);
316        if (!configOK || !configDeferredLoadOK) {
317            HelpUtil.displayHelpRef("package.apps.AppConfigPanelErrorPage");
318            doPreferences();
319        }
320        log.debug("Done with doPreferences, start statusPanel");
321
322        add(statusPanel());
323        log.debug("Done with statusPanel, start buttonSpace");
324        add(buttonSpace());
325        add(_jynstrumentSpace);
326        long eventMask = AWTEvent.MOUSE_EVENT_MASK;
327
328        Toolkit.getDefaultToolkit().addAWTEventListener((AWTEvent e) -> {
329            if (e instanceof MouseEvent) {
330                JmriMouseEvent me = new JmriMouseEvent((MouseEvent) e);
331                if (me.isPopupTrigger() && me.getComponent() instanceof JTextComponent) {
332                    final JTextComponent component1 = (JTextComponent) me.getComponent();
333                    final JPopupMenu menu = new JPopupMenu();
334                    JMenuItem item;
335                    item = new JMenuItem(new DefaultEditorKit.CopyAction());
336                    item.setText("Copy");
337                    item.setEnabled(component1.getSelectionStart() != component1.getSelectionEnd());
338                    menu.add(item);
339                    item = new JMenuItem(new DefaultEditorKit.CutAction());
340                    item.setText("Cut");
341                    item.setEnabled(component1.isEditable() && component1.getSelectionStart() != component1.getSelectionEnd());
342                    menu.add(item);
343                    item = new JMenuItem(new DefaultEditorKit.PasteAction());
344                    item.setText("Paste");
345                    item.setEnabled(component1.isEditable());
346                    menu.add(item);
347                    menu.show(me.getComponent(), me.getX(), me.getY());
348                }
349            }
350        }, eventMask);
351
352        // do final activation
353        InstanceManager.getDefault(jmri.LogixManager.class).activateAllLogixs();
354        InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).initializeLayoutBlockPaths();
355
356        LogixNG_Manager logixNG_Manager = InstanceManager.getDefault(LogixNG_Manager.class);
357        logixNG_Manager.setupAllLogixNGs();
358        if (InstanceManager.getDefault(LogixNGPreferences.class).getStartLogixNGOnStartup()
359                && InstanceManager.getDefault(jmri.jmrit.logixng.LogixNG_Manager.class).isStartLogixNGsOnLoad()) {
360            logixNG_Manager.activateAllLogixNGs();
361        }
362
363        log.debug("End constructor");
364    }
365
366    private boolean doDeferredLoad(File file) {
367        boolean result;
368        log.debug("start deferred load from config");
369        try {
370            ConfigureManager cmOD = InstanceManager.getNullableDefault(jmri.ConfigureManager.class);
371            if (cmOD != null) {
372                result = cmOD.loadDeferred(file);
373            } else {
374                log.error("Failed to get default configure manager");
375                result = false;
376            }
377        } catch (JmriException e) {
378            log.error("Unhandled problem loading deferred configuration", e);
379            result = false;
380        }
381        log.debug("end deferred load from config file, OK={}", result);
382        return result;
383    }
384
385    /**
386     * Prepare the JPanel to contain buttons in the startup GUI. Since it's
387     * possible to add buttons via the preferences, this space may have
388     * additional buttons appended to it later. The default implementation here
389     * just creates an empty space for these to be added to.
390     */
391    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
392            justification = "only one application at a time")
393    protected void setButtonSpace() {
394        _buttonSpace = new JPanel();
395        _buttonSpace.setLayout(new FlowLayout());
396    }
397    static JComponent _jynstrumentSpace = null;
398
399    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
400            justification = "only one application at a time")
401    protected void setJynstrumentSpace() {
402        _jynstrumentSpace = new JPanel();
403        _jynstrumentSpace.setLayout(new FlowLayout());
404        new URIDrop(_jynstrumentSpace, (URI[] uris) -> {
405            for (URI uri : uris ) {
406                ynstrument(new File(uri).getPath());
407            }
408        });
409    }
410
411    public static void ynstrument(String path) {
412        Jynstrument it = JynstrumentFactory.createInstrument(path, _jynstrumentSpace);
413        if (it == null) {
414            log.error("Error while creating Jynstrument {}", path);
415            return;
416        }
417        ThrottleFrame.setTransparent(it);
418        it.setVisible(true);
419        _jynstrumentSpace.setVisible(true);
420        _jynstrumentSpace.add(it);
421    }
422
423    /**
424     * Create default menubar.
425     * <p>
426     * This does not include the development menu.
427     *
428     * @param menuBar Menu bar to be populated
429     * @param wi      WindowInterface where this menu bar will appear
430     */
431    protected void createMenus(JMenuBar menuBar, WindowInterface wi) {
432        if (SystemType.isMacOSX()) {
433            Application.getApplication().setQuitHandler((EventObject eo) -> handleQuit());
434        }
435
436        AppsMainMenu.createMenus(menuBar, wi, this, mainWindowHelpID());
437    }
438
439    /**
440     * Open Preferences action. Often done due to error
441     */
442    public void doPreferences() {
443        if (prefsAction == null) {
444            prefsAction = new TabbedPreferencesAction();
445        }
446        prefsAction.actionPerformed(null);
447    }
448
449    /**
450     * Set the location of the window-specific help for the preferences pane.
451     * Made a separate method so if can be overridden for application specific
452     * preferences help
453     *
454     * @param frame    The frame being described in the help system
455     * @param location The location within the JavaHelp system
456     */
457    protected void setPrefsFrameHelp(JmriJFrame frame, String location) {
458        frame.addHelpMenu(location, true);
459    }
460
461    /**
462     * Returns the ID for the main window's help, which is application specific
463     *
464     * @return help identifier for main window
465     */
466    protected String mainWindowHelpID() {
467        return "package.apps.Apps";
468    }
469
470    protected String line1() {
471        return Bundle.getMessage("DefaultVersionCredit", jmri.Version.name());
472    }
473
474    protected String line2() {
475        return "http://jmri.org/";
476    }
477
478    protected String line3() {
479        return " ";
480    }
481    // line 4
482    JLabel cs4 = new JLabel();
483
484    protected void buildLine4(JPanel pane) {
485        if (connection[0] != null) {
486            buildLine(connection[0], cs4, pane);
487        }
488    }
489    // line 5 optional
490    JLabel cs5 = new JLabel();
491
492    protected void buildLine5(JPanel pane) {
493        if (connection[1] != null) {
494            buildLine(connection[1], cs5, pane);
495        }
496    }
497    // line 6 optional
498    JLabel cs6 = new JLabel();
499
500    protected void buildLine6(JPanel pane) {
501        if (connection[2] != null) {
502            buildLine(connection[2], cs6, pane);
503        }
504    }
505    // line 7 optional
506    JLabel cs7 = new JLabel();
507
508    protected void buildLine7(JPanel pane) {
509        if (connection[3] != null) {
510            buildLine(connection[3], cs7, pane);
511        }
512    }
513
514    protected void buildLine(ConnectionConfig conn, JLabel cs, JPanel pane) {
515        if (conn.name().equals(JmrixConfigPane.NONE)) {
516            cs.setText(" ");
517            return;
518        }
519
520        log.debug("conn.name() is {} ", conn.name()); // eg CAN via MERG Network Interface
521        log.debug("conn.getConnectionName() is {} ", conn.getConnectionName()); // eg MERG2
522        log.debug("conn.getManufacturer() is {} ", conn.getManufacturer()); // eg MERG
523
524        ConnectionStatus.instance().addConnection(conn.getConnectionName(), conn.getInfo());
525        cs.setFont(pane.getFont());
526        updateLine(conn, cs);
527        pane.add(cs);
528    }
529
530    protected void updateLine(ConnectionConfig conn, JLabel cs) {
531        if (conn.getDisabled()) {
532            return;
533        }
534        String name = conn.getConnectionName();
535        if (name == null) {
536            name = conn.getManufacturer();
537        }
538        if (ConnectionStatus.instance().isConnectionOk(name, conn.getInfo())) {
539            cs.setForeground(Color.black);
540            String cf = Bundle.getMessage("ConnectionSucceeded", name, conn.name(), conn.getInfo());
541            cs.setText(cf);
542        } else {
543            cs.setForeground(Color.red);
544            String cf = Bundle.getMessage("ConnectionFailed", name, conn.name(), conn.getInfo());
545            cs.setText(cf);
546        }
547
548        this.revalidate();
549    }
550
551    protected String line8() {
552        return " ";
553    }
554
555    protected String line9() {
556        return Bundle.getMessage("JavaVersionCredit",
557                System.getProperty("java.version", "<unknown>"),
558                Locale.getDefault());
559    }
560
561    protected String logo() {
562        return "resources/logo.gif";
563    }
564
565    /**
566     * Fill in the logo and status panel
567     *
568     * @return Properly-filled out JPanel
569     */
570    protected JPanel statusPanel() {
571        JPanel pane1 = new JPanel();
572        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
573        log.debug("Fetch main logo: {}", logo());
574        pane1.add(new JLabel(new ImageIcon(getToolkit().getImage(FileUtil.findURL(logo(), FileUtil.Location.ALL)), "JMRI logo"), JLabel.LEFT));
575        pane1.add(Box.createRigidArea(new Dimension(15, 0))); // Some spacing between logo and status panel
576
577        log.debug("start labels");
578        JPanel pane2 = new JPanel();
579
580        pane2.setLayout(new BoxLayout(pane2, BoxLayout.Y_AXIS));
581        pane2.add(new JLabel(line1()));
582        pane2.add(new JLabel(line2()));
583        pane2.add(new JLabel(line3()));
584
585        String name = ProfileManager.getDefault().getActiveProfileName();
586        pane2.add(new JLabel(Bundle.getMessage("ActiveProfile", name)));
587
588        // add listener for Com port updates
589        ConnectionStatus.instance().addPropertyChangeListener(this);
590        int i = 0;
591        for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) {
592            if (!conn.getDisabled()) {
593                connection[i] = conn;
594                i++;
595            }
596            if (i > 3) {
597                break;
598            }
599        }
600        buildLine4(pane2);
601        buildLine5(pane2);
602        buildLine6(pane2);
603        buildLine7(pane2);
604
605        pane2.add(new JLabel(line8()));
606        pane2.add(new JLabel(line9()));
607        pane1.add(pane2);
608        return pane1;
609    }
610    //int[] connection = {-1,-1,-1,-1};
611    ConnectionConfig[] connection = {null, null, null, null};
612
613    /**
614     * Closing the main window is a shutdown request.
615     *
616     * @param e the event triggering the close
617     */
618    @Override
619    public void windowClosing(WindowEvent e) {
620        if (!InstanceManager.getDefault(ShutDownManager.class).isShuttingDown()
621                && JmriJOptionPane.YES_OPTION == JmriJOptionPane.showConfirmDialog(
622                        null,
623                        Bundle.getMessage("MessageLongCloseWarning"),
624                        Bundle.getMessage("MessageShortCloseWarning"),
625                        JmriJOptionPane.YES_NO_OPTION)) {
626            handleQuit();
627        }
628        // if get here, didn't quit, so don't close window
629    }
630
631    @Override
632    public void windowActivated(WindowEvent e) {
633    }
634
635    @Override
636    public void windowClosed(WindowEvent e) {
637    }
638
639    @Override
640    public void windowDeactivated(WindowEvent e) {
641    }
642
643    @Override
644    public void windowDeiconified(WindowEvent e) {
645    }
646
647    @Override
648    public void windowIconified(WindowEvent e) {
649    }
650
651    @Override
652    public void windowOpened(WindowEvent e) {
653    }
654
655    static protected void setJmriSystemProperty(String key, String value) {
656        try {
657            String current = System.getProperty("org.jmri.Apps." + key);
658            if (current == null) {
659                System.setProperty("org.jmri.Apps." + key, value);
660            } else if (!current.equals(value)) {
661                log.warn("JMRI property {} already set to {}, skipping reset to {}", key, current, value);
662            }
663        } catch (RuntimeException e) {
664            log.error("Unable to set JMRI property {} to {} due to exception", key, value, e);
665        }
666    }
667
668    /**
669     * Provide access to a place where applications can expect the configuration
670     * code to build run-time buttons.
671     *
672     * @see apps.startup.CreateButtonModelFactory
673     * @return null if no such space exists
674     */
675    static public JComponent buttonSpace() {
676        return _buttonSpace;
677    }
678    static JComponent _buttonSpace = null;
679    static SplashWindow sp = null;
680    static AWTEventListener debugListener = null;
681
682    // TODO: Remove the "static" nature of much of the initialization someday.
683    //       It exits to allow splash() to be called first-thing in main(), see
684    //       apps.DecoderPro.DecoderPro.main(...)
685    //       Or maybe, just not worry about this here, in the older base class,
686    //       and address it in the newer apps.gui3.Apps3 as that's the base class of the future.
687    static boolean debugFired = false;  // true if we've seen F8 during startup
688    static boolean debugmsg = false;    // true while we're handling the "No Logix?" prompt window on startup
689
690    static protected void splash(boolean show) {
691        splash(show, false);
692    }
693
694    static protected void splash(boolean show, boolean debug) {
695        Log4JUtil.initLogging();
696        if (debugListener == null && debug) {
697            // set a global listener for debug options
698            debugFired = false;
699            Toolkit.getDefaultToolkit().addAWTEventListener(
700                    debugListener = (AWTEvent e) -> {
701                        if (!debugFired) {
702                            /*We set the debugmsg flag on the first instance of the user pressing any button
703                            and the if the debugFired hasn't been set, this allows us to ensure that we don't
704                            miss the user pressing F8, while we are checking*/
705                            debugmsg = true;
706                            if (e.getID() == KeyEvent.KEY_PRESSED && e instanceof KeyEvent && ((KeyEvent) e).getKeyCode() == 119) {     // F8
707                                startupDebug();
708                            } else if (e.getID() == KeyEvent.KEY_PRESSED && e instanceof KeyEvent && ((KeyEvent) e).getKeyCode() == 120) {  // F9
709                                InstanceManager.getDefault(LogixNG_Manager.class).startLogixNGsOnLoad(false);
710                            } else {
711                                debugmsg = false;
712                            }
713                        }
714                    },
715                    AWTEvent.KEY_EVENT_MASK);
716        }
717
718        // bring up splash window for startup
719        if (sp == null) {
720            if (debug) {
721                sp = new SplashWindow(splashDebugMsg());
722            } else {
723                sp = new SplashWindow();
724            }
725        }
726        sp.setVisible(show);
727        if (!show) {
728            sp.dispose();
729            Toolkit.getDefaultToolkit().removeAWTEventListener(debugListener);
730            debugListener = null;
731            sp = null;
732        }
733    }
734
735    static protected JPanel splashDebugMsg() {
736        JLabel panelLabelDisableLogix = new JLabel(Bundle.getMessage("PressF8ToDebug"));
737        panelLabelDisableLogix.setFont(panelLabelDisableLogix.getFont().deriveFont(9f));
738        JLabel panelLabelDisableLogixNG = new JLabel(Bundle.getMessage("PressF9ToInactivateLogixNG"));
739        panelLabelDisableLogixNG.setFont(panelLabelDisableLogix.getFont().deriveFont(9f));
740        JPanel panel = new JPanel();
741        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
742        panel.add(panelLabelDisableLogix);
743        panel.add(panelLabelDisableLogixNG);
744        return panel;
745    }
746
747    static protected void startupDebug() {
748        debugFired = true;
749        debugmsg = true;
750
751        Object[] options = {"Disable", "Enable"};
752
753        int retval = JmriJOptionPane.showOptionDialog(null,
754                Bundle.getMessage("StartJMRIwithLogixEnabledDisabled"),
755                Bundle.getMessage("StartJMRIwithLogixEnabledDisabledTitle"),
756                JmriJOptionPane.DEFAULT_OPTION,
757                JmriJOptionPane.QUESTION_MESSAGE, null, options, options[0]);
758
759        if (retval != 0) {
760            debugmsg = false;
761            return;
762        }
763        InstanceManager.getDefault(jmri.LogixManager.class).setLoadDisabled(true);
764        InstanceManager.getDefault(LogixNG_Manager.class).setLoadDisabled(true);
765        log.info("Requested loading with Logixs and LogixNGs disabled.");
766        debugmsg = false;
767    }
768
769    /**
770     * The application decided to quit, handle that.
771     *
772     * @return always returns false
773     */
774    static public boolean handleQuit() {
775        AppsBase.handleQuit();
776        return false;
777    }
778
779    /**
780     * The application decided to restart, handle that.
781     */
782    static public void handleRestart() {
783        AppsBase.handleRestart();
784    }
785
786    /**
787     * Set up the configuration file name at startup.
788     * <p>
789     * The Configuration File name variable holds the name used to load the
790     * configuration file during later startup processing. Applications invoke
791     * this method to handle the usual startup hierarchy:
792     * <ul>
793     * <li>If an absolute filename was provided on the command line, use it
794     * <li>If a filename was provided that's not absolute, consider it to be in
795     * the preferences directory
796     * <li>If no filename provided, use a default name (that's application
797     * specific)
798     * </ul>
799     * This name will be used for reading and writing the preferences. It need
800     * not exist when the program first starts up. This name may be proceeded
801     * with <em>config=</em> and may not contain the equals sign (=).
802     *
803     * @param def  Default value if no other is provided
804     * @param args Argument array from the main routine
805     */
806    static protected void setConfigFilename(String def, String[] args) {
807        // skip if org.jmri.Apps.configFilename is set
808        if (System.getProperty("org.jmri.Apps.configFilename") != null) {
809            return;
810        }
811        // save the configuration filename if present on the command line
812        if (args.length >= 1 && args[0] != null && !args[0].contains("=")) {
813            def = args[0];
814            log.debug("Config file was specified as: {}", args[0]);
815        }
816        for (String arg : args) {
817            String[] split = arg.split("=", 2);
818            if (split[0].equalsIgnoreCase("config")) {
819                def = split[1];
820                log.debug("Config file was specified as: {}", arg);
821            }
822        }
823        Apps.configFilename = def;
824        setJmriSystemProperty("configFilename", def);
825    }
826
827    static public String getConfigFileName() {
828        return configFilename;
829    }
830
831    static protected void createFrame(Apps containedPane, JmriJFrame frame) {
832        // create the main frame and menus
833        // Create a WindowInterface object based on the passed-in Frame
834        JFrameInterface wi = new JFrameInterface(frame);
835        // Create a menu bar
836        containedPane.menuBar = new JMenuBar();
837
838        // Create menu categories and add to the menu bar, add actions to menus
839        containedPane.createMenus(containedPane.menuBar, wi);
840        // connect Help target now that globalHelpBroker has been instantiated
841        containedPane.attachHelp();
842
843        frame.setJMenuBar(containedPane.menuBar);
844        frame.getContentPane().add(containedPane);
845
846        // handle window close
847        frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
848        frame.addWindowListener(containedPane);
849
850        // pack and center this frame
851        frame.pack();
852        Dimension screen = frame.getToolkit().getScreenSize();
853        Dimension size = frame.getSize();
854
855        // first set a default position and size
856        frame.setLocation((screen.width - size.width) / 2, (screen.height - size.height) / 2);
857
858        // then attempt set from stored preference
859        frame.setFrameLocation();
860
861        // and finally show
862        frame.setVisible(true);
863    }
864
865    static protected void loadFile(String name) {
866        ConfigureManager cmOD = InstanceManager.getNullableDefault(jmri.ConfigureManager.class);
867        if (cmOD != null) {
868            URL pFile = cmOD.find(name);
869            if (pFile != null) {
870                try {
871                    cmOD.load(pFile);
872                } catch (JmriException e) {
873                    log.error("Unhandled problem in loadFile", e);
874                }
875            } else {
876                log.warn("Could not find {} config file", name);
877            }
878        } else {
879            log.error("Failed to get default configure manager");
880        }
881    }
882
883    static String configFilename = System.getProperty("org.jmri.Apps.configFilename", "jmriconfig2.xml");  // usually overridden, this is default
884    // The following MUST be protected for 3rd party applications
885    // (such as CATS) which are derived from this class.
886    @SuppressFBWarnings(value = "MS_PKGPROTECT",
887            justification = "The following MUST be protected for 3rd party applications (such as CATS) which are derived from this class.")
888    protected static boolean configOK;
889    @SuppressFBWarnings(value = "MS_PKGPROTECT",
890            justification = "The following MUST be protected for 3rd party applications (such as CATS) which are derived from this class.")
891    protected static boolean configDeferredLoadOK;
892    // GUI members
893    private JMenuBar menuBar;
894
895    static String nameString = "JMRI program";
896
897    protected static void setApplication(String name) {
898        try {
899            jmri.Application.setApplicationName(name);
900        } catch (IllegalArgumentException | IllegalAccessException ex) {
901            log.warn("Unable to set application name", ex);
902        }
903    }
904
905    /**
906     * Set and log some startup information. This is intended to be the central
907     * connection point for common startup and logging.
908     *
909     * @param name Program/application name as known by the user
910     */
911    @SuppressFBWarnings(value = "SLF4J_SIGN_ONLY_FORMAT",justification = "info message contains context information")
912    protected static void setStartupInfo(String name) {
913        // Set the application name
914        try {
915            jmri.Application.setApplicationName(name);
916        } catch (IllegalArgumentException | IllegalAccessException ex) {
917            log.warn("Unable to set application name", ex);
918        }
919
920        // Log the startup information
921        log.info("{}",Log4JUtil.startupInfo(name));
922    }
923
924    @Override
925    public void propertyChange(PropertyChangeEvent ev) {
926        log.debug("property change: comm port status update");
927        if (connection[0] != null) {
928            updateLine(connection[0], cs4);
929        }
930
931        if (connection[1] != null) {
932            updateLine(connection[1], cs5);
933        }
934
935        if (connection[2] != null) {
936            updateLine(connection[2], cs6);
937        }
938
939        if (connection[3] != null) {
940            updateLine(connection[3], cs7);
941        }
942
943    }
944
945    /**
946     * Attach Help target to Help button on Main Screen.
947     */
948    protected void attachHelp() {
949    }
950
951    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Apps.class);
952
953}