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    protected static void setButtonSpace() {
107        _buttonSpace = new JPanel();
108        _buttonSpace.setLayout(new FlowLayout(FlowLayout.LEFT));
109    }
110
111    /**
112     * Provide access to a place where applications can expect the configuration
113     * code to build run-time buttons.
114     *
115     * @see apps.startup.CreateButtonModelFactory
116     * @return null if no such space exists
117     */
118    static public JComponent buttonSpace() {
119        return _buttonSpace;
120    }
121    static JComponent _buttonSpace = null;
122
123    protected JmriJFrame mainFrame;
124
125    abstract protected void createMainFrame();
126
127    public void createAndDisplayFrame() {
128        createMainFrame();
129
130        //A Shutdown manager handles the quiting of the application
131        mainFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
132        displayMainFrame(mainFrame.getMaximumSize());
133    }
134
135    /**
136     * Set a toolbar to be initially floating. This doesn't quite work right.
137     *
138     * @param toolBar the toolbar to float
139     */
140    protected void setFloating(JToolBar toolBar) {
141        //((javax.swing.plaf.basic.BasicToolBarUI) toolBar.getUI()).setFloatingLocation(100,100);
142        ((javax.swing.plaf.basic.BasicToolBarUI) toolBar.getUI()).setFloating(true, new Point(500, 500));
143    }
144
145    protected void displayMainFrame(Dimension d) {
146        mainFrame.setSize(d);
147        mainFrame.setVisible(true);
148    }
149
150    /**
151     * Final actions before releasing control of app to user
152     */
153    @Override
154    protected void start() {
155        // TODO: splash(false);
156        super.start();
157        splash(false);
158    }
159
160    static protected void splash(boolean show) {
161        splash(show, false);
162    }
163
164    static SplashWindow sp = null;
165    static AWTEventListener debugListener = null;
166    static boolean debugFired = false;
167    static boolean debugmsg = false;
168
169    static protected void splash(boolean show, boolean debug) {
170        if (debugListener == null && debug) {
171            // set a global listener for debug options
172            debugFired = false;
173            debugListener = new AWTEventListener() {
174
175                @Override
176                @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "debugmsg write is semi-global")
177                public void eventDispatched(AWTEvent e) {
178                    if (!debugFired) {
179                        /*We set the debugmsg flag on the first instance of the user pressing any button
180                         and the if the debugFired hasn't been set, this allows us to ensure that we don't
181                         miss the user pressing F8, while we are checking*/
182                        debugmsg = true;
183                        if (e.getID() == KeyEvent.KEY_PRESSED && e instanceof KeyEvent && ((KeyEvent) e).getKeyCode() == 119) {     // F8
184                            startupDebug();
185                        } else if (e.getID() == KeyEvent.KEY_PRESSED && e instanceof KeyEvent && ((KeyEvent) e).getKeyCode() == 120) {  // F9
186                            InstanceManager.getDefault(LogixNG_Manager.class).startLogixNGsOnLoad(false);
187                        } else {
188                            debugmsg = false;
189                        }
190                    }
191                }
192            };
193            Toolkit.getDefaultToolkit().addAWTEventListener(debugListener,
194                    AWTEvent.KEY_EVENT_MASK);
195        }
196
197        // bring up splash window for startup
198        if (sp == null) {
199            sp = new SplashWindow((debug) ? splashDebugMsg() : null);
200        }
201        sp.setVisible(show);
202        if (!show) {
203            sp.dispose();
204            Toolkit.getDefaultToolkit().removeAWTEventListener(debugListener);
205            debugListener = null;
206            sp = null;
207        }
208    }
209
210    static protected JPanel splashDebugMsg() {
211        JLabel panelLabelDisableLogix = new JLabel(Bundle.getMessage("PressF8ToDebug"));
212        panelLabelDisableLogix.setFont(panelLabelDisableLogix.getFont().deriveFont(9f));
213        JLabel panelLabelDisableLogixNG = new JLabel(Bundle.getMessage("PressF9ToDisableLogixNG"));
214        panelLabelDisableLogixNG.setFont(panelLabelDisableLogix.getFont().deriveFont(9f));
215        JPanel panel = new JPanel();
216        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
217        panel.add(panelLabelDisableLogix);
218        panel.add(panelLabelDisableLogixNG);
219        return panel;
220    }
221
222    static protected void startupDebug() {
223        debugFired = true;
224        debugmsg = true;
225
226        debugmsg = false;
227    }
228
229    protected void initMacOSXMenus() {
230        apps.plaf.macosx.Application macApp = apps.plaf.macosx.Application.getApplication();
231        macApp.setAboutHandler((EventObject eo) -> {
232            new AboutDialog(null, true).setVisible(true);
233        });
234        macApp.setPreferencesHandler((EventObject eo) -> {
235            new TabbedPreferencesAction(Bundle.getMessage("MenuItemPreferences")).actionPerformed();
236        });
237        macApp.setQuitHandler((EventObject eo) -> handleQuit());
238    }
239
240    /**
241     * Configure the {@link jmri.profile.Profile} to use for this application.
242     * <p>
243     * Overrides super() method so dialogs can be displayed.
244     */
245    @Override
246    protected void configureProfile() {
247        String profileFilename;
248        FileUtil.createDirectory(FileUtil.getPreferencesPath());
249        // Needs to be declared final as we might need to
250        // refer to this on the Swing thread
251        File profileFile;
252        profileFilename = getConfigFileName().replaceFirst(".xml", ".properties");
253        // decide whether name is absolute or relative
254        if (!new File(profileFilename).isAbsolute()) {
255            // must be relative, but we want it to
256            // be relative to the preferences directory
257            profileFile = new File(FileUtil.getPreferencesPath() + profileFilename);
258        } else {
259            profileFile = new File(profileFilename);
260        }
261
262        ProfileManager.getDefault().setConfigFile(profileFile);
263        // See if the profile to use has been specified on the command line as
264        // a system property org.jmri.profile as a profile id.
265        if (System.getProperties().containsKey(ProfileManager.SYSTEM_PROPERTY)) {
266            ProfileManager.getDefault().setActiveProfile(System.getProperty(ProfileManager.SYSTEM_PROPERTY));
267        }
268        // @see jmri.profile.ProfileManager#migrateToProfiles Javadoc for conditions handled here
269        if (!profileFile.exists()) { // no profile config for this app
270            log.trace("profileFile {} doesn't exist", profileFile);
271            try {
272                if (ProfileManager.getDefault().migrateToProfiles(getConfigFileName())) { // migration or first use
273                    // notify user of change only if migration occurred
274                    // TODO: a real migration message
275                    JmriJOptionPane.showMessageDialog(sp,
276                            Bundle.getMessage("ConfigMigratedToProfile"),
277                            jmri.Application.getApplicationName(),
278                            JmriJOptionPane.INFORMATION_MESSAGE);
279                }
280            } catch (IOException | IllegalArgumentException ex) {
281                JmriJOptionPane.showMessageDialog(sp,
282                        ex.getLocalizedMessage(),
283                        jmri.Application.getApplicationName(),
284                        JmriJOptionPane.ERROR_MESSAGE);
285                log.error("Exception: ", ex);
286            }
287        }
288        try {
289            ProfileManagerDialog.getStartingProfile(sp);
290            // Manually setting the configFilename property since calling
291            // Apps.setConfigFilename() does not reset the system property
292            System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME);
293            Profile profile = ProfileManager.getDefault().getActiveProfile();
294            if (profile != null) {
295                log.info("Starting with profile {}", profile.getId());
296            } else {
297                log.info("Starting without a profile");
298            }
299
300            // rapid language set; must follow up later with full setting as part of preferences
301            jmri.util.gui.GuiLafPreferencesManager.setLocaleMinimally(profile);
302        } catch (IOException ex) {
303            log.info("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage());
304        }
305    }
306
307    @Override
308    protected void setAndLoadPreferenceFile() {
309        File sharedConfig = null;
310        try {
311            sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG);
312            if (!sharedConfig.canRead()) {
313                sharedConfig = null;
314            }
315        } catch (FileNotFoundException ex) {
316            // ignore - this only means that sharedConfig does not exist.
317        }
318        super.setAndLoadPreferenceFile();
319        if (sharedConfig == null && configOK == true && configDeferredLoadOK == true) {
320            // this was logged in the super method
321            String name = ProfileManager.getDefault().getActiveProfileName();
322            if (!GraphicsEnvironment.isHeadless()) {
323                JmriJOptionPane.showMessageDialog(sp,
324                        Bundle.getMessage("SingleConfigMigratedToSharedConfig", name),
325                        jmri.Application.getApplicationName(),
326                        JmriJOptionPane.INFORMATION_MESSAGE);
327            }
328        }
329    }
330
331    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Apps3.class);
332
333}