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