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}