001package apps.gui3;
002
003import apps.*;
004import apps.gui3.tabbedpreferences.TabbedPreferencesAction;
005import apps.swing.AboutDialog;
006
007import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
008
009import java.awt.*;
010import java.awt.event.AWTEventListener;
011import java.awt.event.KeyEvent;
012import java.io.*;
013import java.util.EventObject;
014
015import javax.swing.*;
016
017import jmri.InstanceManager;
018import jmri.jmrit.logixng.LogixNG_Manager;
019import jmri.profile.*;
020import jmri.util.*;
021import jmri.util.swing.JmriJOptionPane;
022
023/**
024 * Base class for GUI3 JMRI applications.
025 * <p>
026 * This is a complete re-implementation of the apps.Apps support for JMRI
027 * applications.
028 * <p>
029 * Each using application provides its own main() method.
030 * <p>
031 * There are a large number of missing features marked with TODO in comments
032 * including code from the earlier implementation.
033 *
034 * @author Bob Jacobsen Copyright 2009, 2010
035 */
036public abstract class Apps3 extends AppsBase {
037
038    /**
039     * Initial actions before frame is created, invoked in the applications
040     * main() routine.
041     * <ul>
042     * <li> Operations from {@link AppsBase#preInit(String)}
043     * <li> Initialize the console support
044     * </ul>
045     *
046     * @param applicationName application name
047     */
048    static public void preInit(String applicationName) {
049        AppsBase.preInit(applicationName);
050
051        // Initialise system console
052        // Put this here rather than in apps.AppsBase as this is only relevant
053        // for GUI applications - non-gui apps will use STDOUT & STDERR
054        SystemConsole.getInstance();
055
056        splash(true);
057
058        setButtonSpace();
059
060    }
061
062    /**
063     * Create and initialize the application object.
064     * <p>
065     * Expects initialization from preInit() to already be done.
066     *
067     * @param applicationName application name
068     * @param configFileDef   default configuration file name
069     * @param args            command line arguments set at application launch
070     */
071    public Apps3(String applicationName, String configFileDef, String[] args) {
072        // pre-GUI work
073        super(applicationName, configFileDef, args);
074
075        // create GUI
076        if (SystemType.isMacOSX()) {
077            initMacOSXMenus();
078        }
079        if ( (((!configOK) || (!configDeferredLoadOK)) && (!preferenceFileExists)) || wizardLaunchCheck() ) {
080            launchFirstTimeStartupWizard();
081            return;
082        }
083        createAndDisplayFrame();
084    }
085
086    /**
087     * To be overridden by applications that need to make
088     * additional checks as to whether the first time wizard
089     * should be launched.
090     * @return true to force the wizard to be launched
091     */
092    protected boolean wizardLaunchCheck() {
093        return false;
094    }
095    
096    public void launchFirstTimeStartupWizard() {
097        FirstTimeStartUpWizardAction prefsAction = new FirstTimeStartUpWizardAction("Start Up Wizard");
098        prefsAction.setApp(this);
099        prefsAction.actionPerformed(null);
100    }
101    
102    /**
103     * For compatability with adding in buttons to the toolbar using the
104     * existing createbuttonmodel
105     */
106    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
107            justification = "only one application at a time")
108    protected static void setButtonSpace() {
109        _buttonSpace = new JPanel();
110        _buttonSpace.setLayout(new FlowLayout(FlowLayout.LEFT));
111    }
112
113    /**
114     * Provide access to a place where applications can expect the configuration
115     * code to build run-time buttons.
116     *
117     * @see apps.startup.CreateButtonModelFactory
118     * @return null if no such space exists
119     */
120    static public JComponent buttonSpace() {
121        return _buttonSpace;
122    }
123    static JComponent _buttonSpace = null;
124
125    protected JmriJFrame mainFrame;
126
127    abstract protected void createMainFrame();
128
129    public void createAndDisplayFrame() {
130        createMainFrame();
131
132        //A Shutdown manager handles the quiting of the application
133        mainFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
134        displayMainFrame(mainFrame.getMaximumSize());
135    }
136
137    /**
138     * Set a toolbar to be initially floating. This doesn't quite work right.
139     *
140     * @param toolBar the toolbar to float
141     */
142    protected void setFloating(JToolBar toolBar) {
143        //((javax.swing.plaf.basic.BasicToolBarUI) toolBar.getUI()).setFloatingLocation(100,100);
144        ((javax.swing.plaf.basic.BasicToolBarUI) toolBar.getUI()).setFloating(true, new Point(500, 500));
145    }
146
147    protected void displayMainFrame(Dimension d) {
148        mainFrame.setSize(d);
149        mainFrame.setVisible(true);
150    }
151
152    /**
153     * Final actions before releasing control of app to user
154     */
155    @Override
156    protected void start() {
157        // TODO: splash(false);
158        super.start();
159        splash(false);
160    }
161
162    static protected void splash(boolean show) {
163        splash(show, false);
164    }
165
166    static SplashWindow sp = null;
167    static AWTEventListener debugListener = null;
168    static boolean debugFired = false;
169    static boolean debugmsg = false;
170
171    static protected void splash(boolean show, boolean debug) {
172        if (debugListener == null && debug) {
173            // set a global listener for debug options
174            debugFired = false;
175            debugListener = new AWTEventListener() {
176
177                @Override
178                @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "debugmsg write is semi-global")
179                public void eventDispatched(AWTEvent e) {
180                    if (!debugFired) {
181                        /*We set the debugmsg flag on the first instance of the user pressing any button
182                         and the if the debugFired hasn't been set, this allows us to ensure that we don't
183                         miss the user pressing F8, while we are checking*/
184                        debugmsg = true;
185                        if (e.getID() == KeyEvent.KEY_PRESSED && e instanceof KeyEvent && ((KeyEvent) e).getKeyCode() == 119) {     // F8
186                            startupDebug();
187                        } else if (e.getID() == KeyEvent.KEY_PRESSED && e instanceof KeyEvent && ((KeyEvent) e).getKeyCode() == 120) {  // F9
188                            InstanceManager.getDefault(LogixNG_Manager.class).startLogixNGsOnLoad(false);
189                        } else {
190                            debugmsg = false;
191                        }
192                    }
193                }
194            };
195            Toolkit.getDefaultToolkit().addAWTEventListener(debugListener,
196                    AWTEvent.KEY_EVENT_MASK);
197        }
198
199        // bring up splash window for startup
200        if (sp == null) {
201            sp = new SplashWindow((debug) ? splashDebugMsg() : null);
202        }
203        sp.setVisible(show);
204        if (!show) {
205            sp.dispose();
206            Toolkit.getDefaultToolkit().removeAWTEventListener(debugListener);
207            debugListener = null;
208            sp = null;
209        }
210    }
211
212    static protected JPanel splashDebugMsg() {
213        JLabel panelLabelDisableLogix = new JLabel(Bundle.getMessage("PressF8ToDebug"));
214        panelLabelDisableLogix.setFont(panelLabelDisableLogix.getFont().deriveFont(9f));
215        JLabel panelLabelDisableLogixNG = new JLabel(Bundle.getMessage("PressF9ToDisableLogixNG"));
216        panelLabelDisableLogixNG.setFont(panelLabelDisableLogix.getFont().deriveFont(9f));
217        JPanel panel = new JPanel();
218        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
219        panel.add(panelLabelDisableLogix);
220        panel.add(panelLabelDisableLogixNG);
221        return panel;
222    }
223
224    static protected void startupDebug() {
225        debugFired = true;
226        debugmsg = true;
227
228        debugmsg = false;
229    }
230
231    protected void initMacOSXMenus() {
232        apps.plaf.macosx.Application macApp = apps.plaf.macosx.Application.getApplication();
233        macApp.setAboutHandler((EventObject eo) -> {
234            new AboutDialog(null, true).setVisible(true);
235        });
236        macApp.setPreferencesHandler((EventObject eo) -> {
237            new TabbedPreferencesAction(Bundle.getMessage("MenuItemPreferences")).actionPerformed();
238        });
239        macApp.setQuitHandler((EventObject eo) -> handleQuit());
240    }
241
242    /**
243     * Configure the {@link jmri.profile.Profile} to use for this application.
244     * <p>
245     * Overrides super() method so dialogs can be displayed.
246     */
247    @Override
248    protected void configureProfile() {
249        String profileFilename;
250        FileUtil.createDirectory(FileUtil.getPreferencesPath());
251        // Needs to be declared final as we might need to
252        // refer to this on the Swing thread
253        File profileFile;
254        profileFilename = getConfigFileName().replaceFirst(".xml", ".properties");
255        // decide whether name is absolute or relative
256        if (!new File(profileFilename).isAbsolute()) {
257            // must be relative, but we want it to
258            // be relative to the preferences directory
259            profileFile = new File(FileUtil.getPreferencesPath() + profileFilename);
260        } else {
261            profileFile = new File(profileFilename);
262        }
263
264        ProfileManager.getDefault().setConfigFile(profileFile);
265        // See if the profile to use has been specified on the command line as
266        // a system property org.jmri.profile as a profile id.
267        if (System.getProperties().containsKey(ProfileManager.SYSTEM_PROPERTY)) {
268            ProfileManager.getDefault().setActiveProfile(System.getProperty(ProfileManager.SYSTEM_PROPERTY));
269        }
270        // @see jmri.profile.ProfileManager#migrateToProfiles Javadoc for conditions handled here
271        if (!profileFile.exists()) { // no profile config for this app
272            log.trace("profileFile {} doesn't exist", profileFile);
273            try {
274                if (ProfileManager.getDefault().migrateToProfiles(getConfigFileName())) { // migration or first use
275                    // notify user of change only if migration occurred
276                    // TODO: a real migration message
277                    JmriJOptionPane.showMessageDialog(sp,
278                            Bundle.getMessage("ConfigMigratedToProfile"),
279                            jmri.Application.getApplicationName(),
280                            JmriJOptionPane.INFORMATION_MESSAGE);
281                }
282            } catch (IOException | IllegalArgumentException ex) {
283                JmriJOptionPane.showMessageDialog(sp,
284                        ex.getLocalizedMessage(),
285                        jmri.Application.getApplicationName(),
286                        JmriJOptionPane.ERROR_MESSAGE);
287                log.error("Exception: ", ex);
288            }
289        }
290        try {
291            ProfileManagerDialog.getStartingProfile(sp);
292            // Manually setting the configFilename property since calling
293            // Apps.setConfigFilename() does not reset the system property
294            System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME);
295            Profile profile = ProfileManager.getDefault().getActiveProfile();
296            if (profile != null) {
297                log.info("Starting with profile {}", profile.getId());
298            } else {
299                log.info("Starting without a profile");
300            }
301
302            // rapid language set; must follow up later with full setting as part of preferences
303            jmri.util.gui.GuiLafPreferencesManager.setLocaleMinimally(profile);
304        } catch (IOException ex) {
305            log.info("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage());
306        }
307    }
308
309    @Override
310    protected void setAndLoadPreferenceFile() {
311        File sharedConfig = null;
312        try {
313            sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG);
314            if (!sharedConfig.canRead()) {
315                sharedConfig = null;
316            }
317        } catch (FileNotFoundException ex) {
318            // ignore - this only means that sharedConfig does not exist.
319        }
320        super.setAndLoadPreferenceFile();
321        if (sharedConfig == null && configOK == true && configDeferredLoadOK == true) {
322            // this was logged in the super method
323            String name = ProfileManager.getDefault().getActiveProfileName();
324            if (!GraphicsEnvironment.isHeadless()) {
325                JmriJOptionPane.showMessageDialog(sp,
326                        Bundle.getMessage("SingleConfigMigratedToSharedConfig", name),
327                        jmri.Application.getApplicationName(),
328                        JmriJOptionPane.INFORMATION_MESSAGE);
329            }
330        }
331    }
332
333    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Apps3.class);
334
335}