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