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 menuBar.add(valuesMenu, pos + offset + 2); // put it to the right of the Speed menu 164 165 } 166 167 /** 168 * Save the restore rule selection. Called by menu item change events. 169 * 170 * @param newRule The RestoreRule enum constant 171 */ 172 void setRestoreRule(RestoreRule newRule) { 173 _restoreRule = newRule; 174 InstanceManager.getDefault(jmri.UserPreferencesManager.class). 175 setProperty(getClassName(), "Restore Rule", newRule.name()); // NOI18N 176 } 177 178 /** 179 * Retrieve the restore rule selection from user preferences 180 * 181 * @return restoreRule 182 */ 183 public static RestoreRule getRestoreRule() { 184 RestoreRule rr = RestoreRule.RESTOREONLYIFALLOCCUPIED; //default to previous JMRI behavior 185 Object rro = InstanceManager.getDefault(jmri.UserPreferencesManager.class). 186 getProperty("jmri.jmrit.beantable.BlockTableAction", "Restore Rule"); // NOI18N 187 if (rro != null) { 188 try { 189 rr = RestoreRule.valueOf(rro.toString()); 190 } catch (IllegalArgumentException ignored) { 191 log.warn("Invalid Block Restore Rule value '{}' ignored", rro); // NOI18N 192 } 193 } 194 return rr; 195 } 196 197 private void metricSelectionChanged() { 198 InstanceManager.getDefault(UserPreferencesManager.class) 199 .setSimplePreferenceState(BLOCK_METRIC_PREF, centimeterBox.isSelected()); 200 ((BlockTableDataModel)m).setMetric(centimeterBox.isSelected()); 201 } 202 203 @Override 204 protected String helpTarget() { 205 return "package.jmri.jmrit.beantable.BlockTable"; 206 } 207 208 private JmriJFrame addFrame = null; 209 private final JTextField sysName = new JTextField(20); 210 private final JTextField userName = new JTextField(20); 211 212 private final SpinnerNumberModel numberToAddSpinnerNumberModel = 213 new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items 214 private final JSpinner numberToAddSpinner = new JSpinner(numberToAddSpinnerNumberModel); 215 private final JCheckBox addRangeCheckBox = new JCheckBox(Bundle.getMessage("AddRangeBox")); 216 private final JCheckBox _autoSystemNameCheckBox = new JCheckBox(Bundle.getMessage("LabelAutoSysName")); 217 private final JLabel statusBar = new JLabel(Bundle.getMessage("AddBeanStatusEnter"), SwingConstants.LEADING); 218 private JButton newButton = null; 219 220 /** 221 * Rules for restoring block values * 222 */ 223 public enum RestoreRule { 224 RESTOREALWAYS, 225 RESTOREOCCUPIEDONLY, 226 RESTOREONLYIFALLOCCUPIED; 227 } 228 229 private RestoreRule _restoreRule; 230 231 @Override 232 protected void addPressed(ActionEvent e) { 233 if (addFrame == null) { 234 addFrame = new JmriJFrame(Bundle.getMessage("TitleAddBlock"), false, true); 235 addFrame.setEscapeKeyClosesWindow(true); 236 addFrame.addHelpMenu("package.jmri.jmrit.beantable.BlockAddEdit", true); // NOI18N 237 addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS)); 238 ActionListener oklistener = this::okPressed; 239 ActionListener cancellistener = this::cancelPressed; 240 241 AddNewBeanPanel anbp = new AddNewBeanPanel(sysName, userName, 242 numberToAddSpinner, addRangeCheckBox, _autoSystemNameCheckBox, 243 "ButtonCreate", oklistener, cancellistener, statusBar); 244 addFrame.add(anbp); 245 newButton = anbp.ok; 246 sysName.setToolTipText(Bundle.getMessage("SysNameToolTip", "B")); 247 } 248 sysName.setBackground(Color.white); 249 // reset statusBar text 250 statusBar.setText(Bundle.getMessage("AddBeanStatusEnter")); 251 statusBar.setForeground(Color.gray); 252 if (InstanceManager.getDefault(jmri.UserPreferencesManager.class).getSimplePreferenceState(systemNameAuto)) { 253 _autoSystemNameCheckBox.setSelected(true); 254 } 255 if (newButton!=null){ 256 addFrame.getRootPane().setDefaultButton(newButton); 257 } 258 addRangeCheckBox.setSelected(false); 259 addFrame.pack(); 260 addFrame.setVisible(true); 261 } 262 263 private final String systemNameAuto = this.getClass().getName() + ".AutoSystemName"; 264 265 void cancelPressed(ActionEvent e) { 266 addFrame.setVisible(false); 267 addFrame.dispose(); 268 addFrame = null; 269 } 270 271 /** 272 * Respond to Create new item pressed on Add Block pane. 273 * 274 * @param e the click event 275 */ 276 void okPressed(ActionEvent e) { 277 278 int numberOfBlocks = 1; 279 280 if (addRangeCheckBox.isSelected()) { 281 numberOfBlocks = (Integer) numberToAddSpinner.getValue(); 282 } 283 if ( numberOfBlocks >= 65 // limited by JSpinnerModel to 100 284 && JmriJOptionPane.showConfirmDialog(addFrame, 285 Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("Blocks"), numberOfBlocks), 286 Bundle.getMessage("WarningTitle"), 287 JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) { 288 return; 289 } 290 String user = NamedBean.normalizeUserName(userName.getText()); 291 if (user == null || user.isEmpty()) { 292 user = null; 293 } 294 String uName = user; // keep result separate to prevent recursive manipulation 295 String system = ""; 296 297 if (!_autoSystemNameCheckBox.isSelected()) { 298 system = InstanceManager.getDefault(jmri.BlockManager.class).makeSystemName(sysName.getText()); 299 } 300 String sName = system; // keep result separate to prevent recursive manipulation 301 // initial check for empty entry using the raw name 302 if (sName.length() < 3 && !_autoSystemNameCheckBox.isSelected()) { // Using 3 to catch a plain IB 303 statusBar.setText(Bundle.getMessage("WarningSysNameEmpty")); 304 statusBar.setForeground(Color.red); 305 sysName.setBackground(Color.red); 306 return; 307 } else { 308 sysName.setBackground(Color.white); 309 } 310 311 // Add some entry pattern checking, before assembling sName and handing it to the blockManager 312 StringBuilder statusMessage = new StringBuilder( 313 Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameBlock"))); 314 315 for (int x = 0; x < numberOfBlocks; x++) { 316 if (x != 0) { // start at 2nd Block 317 if (!_autoSystemNameCheckBox.isSelected()) { 318 // Find first block with unused system name 319 while (true) { 320 system = nextName(system); 321 log.debug("Trying sys {}", system); 322 Block blk = InstanceManager.getDefault(BlockManager.class).getBySystemName(system); 323 if (blk == null) { 324 sName = system; 325 break; 326 } 327 } 328 } 329 if (user != null) { 330 // Find first block with unused user name 331 while (true) { 332 user = nextName(user); 333 log.debug("Trying user {}", user); 334 Block blk = InstanceManager.getDefault(BlockManager.class).getByUserName(user); 335 if (blk == null) { 336 uName = user; 337 break; 338 } 339 } 340 } 341 } 342 Block blk; 343 String xName = ""; 344 try { 345 if (_autoSystemNameCheckBox.isSelected()) { 346 blk = InstanceManager.getDefault(BlockManager.class).createNewBlock(uName); 347 if (blk == null) { 348 xName = uName; 349 throw new java.lang.IllegalArgumentException(); 350 } 351 } else { 352 blk = InstanceManager.getDefault(BlockManager.class).createNewBlock(sName, uName); 353 if (blk == null) { 354 xName = sName; 355 throw new java.lang.IllegalArgumentException(); 356 } 357 } 358 } catch (IllegalArgumentException ex) { 359 // user input no good 360 handleCreateException(xName); 361 statusBar.setText(Bundle.getMessage("ErrorAddFailedCheck")); 362 statusBar.setForeground(Color.red); 363 return; // without creating 364 } 365 366 // add first and last names to statusMessage user feedback string 367 if (x == 0 || x == numberOfBlocks - 1) { 368 statusMessage.append(" ").append(sName).append(" (").append(user).append(")"); 369 } 370 if (x == numberOfBlocks - 2) { 371 statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" "); 372 } 373 // only mention first and last of addRangeCheckBox added 374 } // end of for loop creating addRangeCheckBox of Blocks 375 376 // provide feedback to user 377 statusBar.setText(statusMessage.toString()); 378 statusBar.setForeground(Color.gray); 379 380 InstanceManager.getDefault(UserPreferencesManager.class) 381 .setSimplePreferenceState(systemNameAuto, _autoSystemNameCheckBox.isSelected()); 382 } 383 384 void handleCreateException(String sysName) { 385 JmriJOptionPane.showMessageDialog(addFrame, 386 Bundle.getMessage("ErrorBlockAddFailed", sysName) + "\n" + Bundle.getMessage("ErrorAddFailedCheck"), 387 Bundle.getMessage("ErrorTitle"), 388 JmriJOptionPane.ERROR_MESSAGE); 389 } 390 391 void deletePaths(JmriJFrame f) { 392 // Set option to prevent the path information from being saved. 393 394 Object[] options = {Bundle.getMessage("ButtonRemove"), 395 Bundle.getMessage("ButtonKeep")}; 396 397 int retval = JmriJOptionPane.showOptionDialog(f, 398 Bundle.getMessage("BlockPathMessage"), 399 Bundle.getMessage("BlockPathSaveTitle"), 400 JmriJOptionPane.YES_NO_OPTION, 401 JmriJOptionPane.QUESTION_MESSAGE, null, options, options[1]); 402 if (retval != 0) { 403 InstanceManager.getDefault(BlockManager.class).setSavedPathInfo(true); 404 log.info("Requested to save path information via Block Menu."); 405 } else { 406 InstanceManager.getDefault(BlockManager.class).setSavedPathInfo(false); 407 log.info("Requested not to save path information via Block Menu."); 408 } 409 } 410 411 @Override 412 public String getClassDescription() { 413 return Bundle.getMessage("TitleBlockTable"); 414 } 415 416 @Override 417 protected String getClassName() { 418 return BlockTableAction.class.getName(); 419 } 420 421 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockTableAction.class); 422 423}