001package jmri.jmrit.beantable; 002 003import java.awt.Color; 004import java.awt.event.ActionEvent; 005import java.awt.event.ActionListener; 006 007import javax.annotation.Nonnull; 008import javax.swing.*; 009 010import jmri.Block; 011import jmri.InstanceManager; 012import jmri.Manager; 013import jmri.NamedBean; 014import jmri.UserPreferencesManager; 015import jmri.jmrit.beantable.block.BlockTableDataModel; 016import jmri.BlockManager; 017import jmri.util.JmriJFrame; 018import jmri.util.swing.JmriJOptionPane; 019 020/** 021 * Swing action to create and register a BlockTable GUI. 022 * 023 * @author Bob Jacobsen Copyright (C) 2003, 2008 024 * @author Egbert Broerse Copyright (C) 2017 025 */ 026public class BlockTableAction extends AbstractTableAction<Block> { 027 028 /** 029 * Create an action with a specific title. 030 * <p> 031 * Note that the argument is the Action title, not the title of the 032 * resulting frame. Perhaps this should be changed? 033 * 034 * @param actionName the Action title 035 */ 036 public BlockTableAction(String actionName) { 037 super(actionName); 038 039 // disable ourself if there is no primary Block manager available 040 if (InstanceManager.getNullableDefault(BlockManager.class) == null) { 041 BlockTableAction.this.setEnabled(false); 042 } 043 } 044 045 public BlockTableAction() { 046 this(Bundle.getMessage("TitleBlockTable")); 047 } 048 049 /** 050 * Create the JTable DataModel, along with the changes for the specific case 051 * of Block objects. 052 */ 053 @Override 054 protected void createModel() { 055 m = new BlockTableDataModel(getManager()); 056 } 057 058 @Nonnull 059 @Override 060 protected Manager<Block> getManager() { 061 return InstanceManager.getDefault(BlockManager.class); 062 } 063 064 @Override 065 protected void setTitle() { 066 f.setTitle(Bundle.getMessage("TitleBlockTable")); // NOI18N 067 } 068 069 private final JRadioButton inchBox = new JRadioButton(Bundle.getMessage("LengthInches")); // NOI18N 070 private final JRadioButton centimeterBox = new JRadioButton(Bundle.getMessage("LengthCentimeters")); // NOI18N 071 public static final String BLOCK_METRIC_PREF = BlockTableAction.class.getName() + ":LengthUnitMetric"; // NOI18N 072 073 private void initRadioButtons(){ 074 075 inchBox.setToolTipText(Bundle.getMessage("InchBoxToolTip")); // NOI18N 076 centimeterBox.setToolTipText(Bundle.getMessage("CentimeterBoxToolTip")); // NOI18N 077 078 ButtonGroup group = new ButtonGroup(); 079 group.add(inchBox); 080 group.add(centimeterBox); 081 inchBox.setSelected(true); 082 centimeterBox.setSelected( InstanceManager.getDefault(UserPreferencesManager.class) 083 .getSimplePreferenceState(BLOCK_METRIC_PREF)); 084 085 inchBox.addActionListener( e -> metricSelectionChanged()); 086 centimeterBox.addActionListener( e -> metricSelectionChanged()); 087 088 // disabling keyboard input as when focused, does not fire actionlistener 089 // and appears selected causing mismatch with button selected and what the table thinks is selected. 090 inchBox.setFocusable(false); 091 centimeterBox.setFocusable(false); 092 } 093 094 /** 095 * Add the radioButtons (only 1 may be selected). 096 */ 097 @Override 098 public void addToFrame(BeanTableFrame<Block> f) { 099 initRadioButtons(); 100 f.addToBottomBox(inchBox, this.getClass().getName()); 101 f.addToBottomBox(centimeterBox, this.getClass().getName()); 102 } 103 104 /** 105 * Insert 2 table specific menus. 106 * <p> 107 * Account for the Window and Help menus, 108 * which are already added to the menu bar as part of the creation of the 109 * JFrame, by adding the menus 2 places earlier unless the table is part of 110 * the ListedTableFrame, that adds the Help menu later on. 111 * 112 * @param f the JFrame of this table 113 */ 114 @Override 115 public void setMenuBar(BeanTableFrame<Block> f) { 116 final JmriJFrame finalF = f; // needed for anonymous ActionListener class 117 JMenuBar menuBar = f.getJMenuBar(); 118 // count the number of menus to insert the TableMenus before 'Window' and 'Help' 119 int pos = menuBar.getMenuCount() - 1; 120 int offset = 1; 121 log.debug("setMenuBar number of menu items = {}", pos); 122 for (int i = 0; i <= pos; i++) { 123 var comp = menuBar.getComponent(i); 124 if ( comp instanceof JMenu 125 && ((JMenu)comp).getText().equals(Bundle.getMessage("MenuHelp"))) { 126 offset = -1; // correct for use as part of ListedTableAction where the Help Menu is not yet present 127 } 128 } 129 _restoreRule = getRestoreRule(); 130 131 JMenu pathMenu = new JMenu(Bundle.getMessage("MenuPaths")); 132 JMenuItem item = new JMenuItem(Bundle.getMessage("MenuItemDeletePaths")); 133 pathMenu.add(item); 134 item.addActionListener( e -> deletePaths(finalF) ); 135 menuBar.add(pathMenu, pos + offset); 136 137 JMenu speedMenu = new JMenu(Bundle.getMessage("SpeedsMenu")); 138 item = new JMenuItem(Bundle.getMessage("SpeedsMenuItemDefaults")); 139 speedMenu.add(item); 140 item.addActionListener( e -> ((BlockTableDataModel)m).setDefaultSpeeds(finalF)); 141 menuBar.add(speedMenu, pos + offset + 1); // put it to the right of the Paths menu 142 143 JMenu valuesMenu = new JMenu(Bundle.getMessage("ValuesMenu")); 144 ButtonGroup valuesButtonGroup = new ButtonGroup(); 145 JRadioButtonMenuItem jrbmi = new JRadioButtonMenuItem(Bundle.getMessage("ValuesMenuRestoreAlways")); // NOI18N 146 jrbmi.addItemListener( e -> setRestoreRule(RestoreRule.RESTOREALWAYS) ); 147 valuesButtonGroup.add(jrbmi); 148 valuesMenu.add(jrbmi); 149 jrbmi.setSelected(_restoreRule == RestoreRule.RESTOREALWAYS); 150 151 jrbmi = new JRadioButtonMenuItem(Bundle.getMessage("ValuesMenuRestoreOccupiedOnly")); // NOI18N 152 jrbmi.addItemListener( e -> setRestoreRule(RestoreRule.RESTOREOCCUPIEDONLY) ); 153 valuesButtonGroup.add(jrbmi); 154 valuesMenu.add(jrbmi); 155 jrbmi.setSelected(_restoreRule == RestoreRule.RESTOREOCCUPIEDONLY); 156 157 jrbmi = new JRadioButtonMenuItem(Bundle.getMessage("ValuesMenuRestoreOnlyIfAllOccupied")); // NOI18N 158 jrbmi.addItemListener( e -> setRestoreRule(RestoreRule.RESTOREONLYIFALLOCCUPIED) ); 159 valuesButtonGroup.add(jrbmi); 160 valuesMenu.add(jrbmi); 161 jrbmi.setSelected(_restoreRule == RestoreRule.RESTOREONLYIFALLOCCUPIED); 162 163 valuesMenu.addSeparator(); 164 165 item = new JMenuItem(Bundle.getMessage("MenuItemReloadBlockValues")); 166 valuesMenu.add(item); 167 item.addActionListener(e -> reloadBlockValues()); 168 169 menuBar.add(valuesMenu, pos + offset + 2); // put it to the right of the Speed menu 170 } 171 172 /** 173 * Save the restore rule selection. Called by menu item change events. 174 * 175 * @param newRule The RestoreRule enum constant 176 */ 177 void setRestoreRule(RestoreRule newRule) { 178 _restoreRule = newRule; 179 InstanceManager.getDefault(jmri.UserPreferencesManager.class). 180 setProperty(getClassName(), "Restore Rule", newRule.name()); // NOI18N 181 } 182 183 /** 184 * Retrieve the restore rule selection from user preferences 185 * 186 * @return restoreRule 187 */ 188 public static RestoreRule getRestoreRule() { 189 RestoreRule rr = RestoreRule.RESTOREONLYIFALLOCCUPIED; //default to previous JMRI behavior 190 Object rro = InstanceManager.getDefault(jmri.UserPreferencesManager.class). 191 getProperty("jmri.jmrit.beantable.BlockTableAction", "Restore Rule"); // NOI18N 192 if (rro != null) { 193 try { 194 rr = RestoreRule.valueOf(rro.toString()); 195 } catch (IllegalArgumentException ignored) { 196 log.warn("Invalid Block Restore Rule value '{}' ignored", rro); // NOI18N 197 } 198 } 199 return rr; 200 } 201 202 private void metricSelectionChanged() { 203 InstanceManager.getDefault(UserPreferencesManager.class) 204 .setSimplePreferenceState(BLOCK_METRIC_PREF, centimeterBox.isSelected()); 205 ((BlockTableDataModel)m).setMetric(centimeterBox.isSelected()); 206 } 207 208 private void reloadBlockValues() { 209 try { 210 new jmri.jmrit.display.layoutEditor.BlockValueFile().readBlockValues(); 211 } catch (org.jdom2.JDOMException jde) { 212 log.error("JDOM Exception when retreiving block values", jde); 213 } catch (java.io.IOException ioe) { 214 log.error("I/O Exception when retreiving block values", ioe); 215 } 216 } 217 218 @Override 219 protected String helpTarget() { 220 return "package.jmri.jmrit.beantable.BlockTable"; 221 } 222 223 private JmriJFrame addFrame = null; 224 private final JTextField sysName = new JTextField(20); 225 private final JTextField userName = new JTextField(20); 226 227 private final SpinnerNumberModel numberToAddSpinnerNumberModel = 228 new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items 229 private final JSpinner numberToAddSpinner = new JSpinner(numberToAddSpinnerNumberModel); 230 private final JCheckBox addRangeCheckBox = new JCheckBox(Bundle.getMessage("AddRangeBox")); 231 private final JCheckBox _autoSystemNameCheckBox = new JCheckBox(Bundle.getMessage("LabelAutoSysName")); 232 private final JLabel statusBar = new JLabel(Bundle.getMessage("AddBeanStatusEnter"), SwingConstants.LEADING); 233 private JButton newButton = null; 234 235 /** 236 * Rules for restoring block values * 237 */ 238 public enum RestoreRule { 239 RESTOREALWAYS, 240 RESTOREOCCUPIEDONLY, 241 RESTOREONLYIFALLOCCUPIED; 242 } 243 244 private RestoreRule _restoreRule; 245 246 @Override 247 protected void addPressed(ActionEvent e) { 248 if (addFrame == null) { 249 addFrame = new JmriJFrame(Bundle.getMessage("TitleAddBlock"), false, true); 250 addFrame.setEscapeKeyClosesWindow(true); 251 addFrame.addHelpMenu("package.jmri.jmrit.beantable.BlockAddEdit", true); // NOI18N 252 addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS)); 253 ActionListener oklistener = this::okPressed; 254 ActionListener cancellistener = this::cancelPressed; 255 256 AddNewBeanPanel anbp = new AddNewBeanPanel(sysName, userName, 257 numberToAddSpinner, addRangeCheckBox, _autoSystemNameCheckBox, 258 "ButtonCreate", oklistener, cancellistener, statusBar); 259 addFrame.add(anbp); 260 newButton = anbp.ok; 261 sysName.setToolTipText(Bundle.getMessage("SysNameToolTip", "B")); 262 } 263 sysName.setBackground(Color.white); 264 // reset statusBar text 265 statusBar.setText(Bundle.getMessage("AddBeanStatusEnter")); 266 statusBar.setForeground(Color.gray); 267 if (InstanceManager.getDefault(jmri.UserPreferencesManager.class).getSimplePreferenceState(systemNameAuto)) { 268 _autoSystemNameCheckBox.setSelected(true); 269 } 270 if (newButton!=null){ 271 addFrame.getRootPane().setDefaultButton(newButton); 272 } 273 addRangeCheckBox.setSelected(false); 274 addFrame.pack(); 275 addFrame.setVisible(true); 276 } 277 278 private final String systemNameAuto = this.getClass().getName() + ".AutoSystemName"; 279 280 void cancelPressed(ActionEvent e) { 281 addFrame.setVisible(false); 282 addFrame.dispose(); 283 addFrame = null; 284 } 285 286 /** 287 * Respond to Create new item pressed on Add Block pane. 288 * 289 * @param e the click event 290 */ 291 void okPressed(ActionEvent e) { 292 293 int numberOfBlocks = 1; 294 295 if (addRangeCheckBox.isSelected()) { 296 numberOfBlocks = (Integer) numberToAddSpinner.getValue(); 297 } 298 if ( numberOfBlocks >= 65 // limited by JSpinnerModel to 100 299 && JmriJOptionPane.showConfirmDialog(addFrame, 300 Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("Blocks"), numberOfBlocks), 301 Bundle.getMessage("WarningTitle"), 302 JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) { 303 return; 304 } 305 String user = NamedBean.normalizeUserName(userName.getText()); 306 if (user == null || user.isEmpty()) { 307 user = null; 308 } 309 String uName = user; // keep result separate to prevent recursive manipulation 310 String system = ""; 311 312 if (!_autoSystemNameCheckBox.isSelected()) { 313 system = InstanceManager.getDefault(jmri.BlockManager.class).makeSystemName(sysName.getText()); 314 } 315 String sName = system; // keep result separate to prevent recursive manipulation 316 // initial check for empty entry using the raw name 317 if (sName.length() < 3 && !_autoSystemNameCheckBox.isSelected()) { // Using 3 to catch a plain IB 318 statusBar.setText(Bundle.getMessage("WarningSysNameEmpty")); 319 statusBar.setForeground(Color.red); 320 sysName.setBackground(Color.red); 321 return; 322 } else { 323 sysName.setBackground(Color.white); 324 } 325 326 // Add some entry pattern checking, before assembling sName and handing it to the blockManager 327 StringBuilder statusMessage = new StringBuilder( 328 Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameBlock"))); 329 330 for (int x = 0; x < numberOfBlocks; x++) { 331 if (x != 0) { // start at 2nd Block 332 if (!_autoSystemNameCheckBox.isSelected()) { 333 // Find first block with unused system name 334 while (true) { 335 system = nextName(system); 336 log.debug("Trying sys {}", system); 337 Block blk = InstanceManager.getDefault(BlockManager.class).getBySystemName(system); 338 if (blk == null) { 339 sName = system; 340 break; 341 } 342 } 343 } 344 if (user != null) { 345 // Find first block with unused user name 346 while (true) { 347 user = nextName(user); 348 log.debug("Trying user {}", user); 349 Block blk = InstanceManager.getDefault(BlockManager.class).getByUserName(user); 350 if (blk == null) { 351 uName = user; 352 break; 353 } 354 } 355 } 356 } 357 Block blk; 358 String xName = ""; 359 try { 360 if (_autoSystemNameCheckBox.isSelected()) { 361 blk = InstanceManager.getDefault(BlockManager.class).createNewBlock(uName); 362 if (blk == null) { 363 xName = uName; 364 throw new java.lang.IllegalArgumentException(); 365 } 366 } else { 367 blk = InstanceManager.getDefault(BlockManager.class).createNewBlock(sName, uName); 368 if (blk == null) { 369 xName = sName; 370 throw new java.lang.IllegalArgumentException(); 371 } 372 } 373 } catch (IllegalArgumentException ex) { 374 // user input no good 375 handleCreateException(xName); 376 statusBar.setText(Bundle.getMessage("ErrorAddFailedCheck")); 377 statusBar.setForeground(Color.red); 378 return; // without creating 379 } 380 381 // add first and last names to statusMessage user feedback string 382 if (x == 0 || x == numberOfBlocks - 1) { 383 statusMessage.append(" ").append(sName).append(" (").append(user).append(")"); 384 } 385 if (x == numberOfBlocks - 2) { 386 statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" "); 387 } 388 // only mention first and last of addRangeCheckBox added 389 } // end of for loop creating addRangeCheckBox of Blocks 390 391 // provide feedback to user 392 statusBar.setText(statusMessage.toString()); 393 statusBar.setForeground(Color.gray); 394 395 InstanceManager.getDefault(UserPreferencesManager.class) 396 .setSimplePreferenceState(systemNameAuto, _autoSystemNameCheckBox.isSelected()); 397 } 398 399 void handleCreateException(String sysName) { 400 JmriJOptionPane.showMessageDialog(addFrame, 401 Bundle.getMessage("ErrorBlockAddFailed", sysName) + "\n" + Bundle.getMessage("ErrorAddFailedCheck"), 402 Bundle.getMessage("ErrorTitle"), 403 JmriJOptionPane.ERROR_MESSAGE); 404 } 405 406 void deletePaths(JmriJFrame f) { 407 // Set option to prevent the path information from being saved. 408 409 Object[] options = {Bundle.getMessage("ButtonRemove"), 410 Bundle.getMessage("ButtonKeep")}; 411 412 int retval = JmriJOptionPane.showOptionDialog(f, 413 Bundle.getMessage("BlockPathMessage"), 414 Bundle.getMessage("BlockPathSaveTitle"), 415 JmriJOptionPane.YES_NO_OPTION, 416 JmriJOptionPane.QUESTION_MESSAGE, null, options, options[1]); 417 if (retval != 0) { 418 InstanceManager.getDefault(BlockManager.class).setSavedPathInfo(true); 419 log.info("Requested to save path information via Block Menu."); 420 } else { 421 InstanceManager.getDefault(BlockManager.class).setSavedPathInfo(false); 422 log.info("Requested not to save path information via Block Menu."); 423 } 424 } 425 426 @Override 427 public String getClassDescription() { 428 return Bundle.getMessage("TitleBlockTable"); 429 } 430 431 @Override 432 protected String getClassName() { 433 return BlockTableAction.class.getName(); 434 } 435 436 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockTableAction.class); 437 438}