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