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