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