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