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}