001package jmri.jmrit.beantable.oblock; 002 003import javax.annotation.CheckForNull; 004import javax.annotation.Nonnull; 005import javax.swing.*; 006import java.awt.*; 007import java.awt.event.ActionEvent; 008 009import jmri.InstanceManager; 010import jmri.UserPreferencesManager; 011import jmri.jmrit.logix.*; 012import jmri.util.JmriJFrame; 013import jmri.util.swing.JmriJOptionPane; 014 015/** 016 * Defines a GUI for editing OBlock - OPath objects in the _tabbed OBlock Table interface. 017 * Based on {@link jmri.jmrit.audio.swing.AudioSourceFrame} and 018 * {@link jmri.jmrit.beantable.routetable.AbstractRouteAddEditFrame} 019 * 020 * @author Matthew Harris copyright (c) 2009 021 * @author Egbert Broerse (C) 2020 022 */ 023public class BlockPathEditFrame extends JmriJFrame { 024 025 // UI components for Add/Edit Path 026 JLabel blockLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanNameOBlock")), JLabel.TRAILING); 027 JLabel blockName = new JLabel(); 028 JLabel pathLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("PathName")), JLabel.TRAILING); 029 protected JTextField pathUserName = new JTextField(15); 030 JLabel fromPortalLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("FromPortal")), JLabel.TRAILING); 031 JLabel toPortalLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("ToPortal")), JLabel.TRAILING); 032 String[] p0 = {""}; 033 protected final JComboBox<String> fromPortalComboBox = new JComboBox<>(p0); 034 protected final JComboBox<String> toPortalComboBox = new JComboBox<>(p0); 035 JLabel statusBar = new JLabel(Bundle.getMessage("AddXStatusInitial1", Bundle.getMessage("Path"), Bundle.getMessage("ButtonOK")), JLabel.LEADING); 036 // the following 3 items copied from beanedit, place in separate static method? 037 private final JSpinner lengthSpinner = new JSpinner(); // 2 digit decimal format field, initialized later as instance 038 private final JRadioButton inch = new JRadioButton(Bundle.getMessage("LengthInches")); 039 private final JRadioButton cm = new JRadioButton(Bundle.getMessage("LengthCentimeters")); 040 041 private final BlockPathEditFrame frame = this; 042 private boolean _newPath = false; 043 //protected final OBlockManager obm = InstanceManager.getDefault(OBlockManager.class); 044 PortalManager pm; 045 private final OBlock _block; 046 private OPath _path; 047 TableFrames _core; 048 BlockPathTableModel _pathmodel; 049 PathTurnoutTableModel _tomodel; 050 TableFrames.PathTurnoutJPanel _turnoutTablePane; 051 052 protected UserPreferencesManager pref; 053 protected boolean isDirty = false; // true to fire reminder to save work 054 private boolean checkEnabled = jmri.InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).isStoreCheckEnabled(); 055 056// @SuppressWarnings("OverridableMethodCallInConstructor") 057 public BlockPathEditFrame(String title, @Nonnull OBlock block, @CheckForNull OPath path, 058 @CheckForNull TableFrames.PathTurnoutJPanel turnouttable, BlockPathTableModel pathmodel, TableFrames parent) { 059 super(title, true, true); 060 _block = block; 061 _turnoutTablePane = turnouttable; 062 _pathmodel = pathmodel; 063 _core = parent; 064 if (path == null || turnouttable == null) { 065 _newPath = true; 066 } else { 067 if ((path.getBlock() != null) && (path.getBlock() != block)) { 068 // somehow we received a path that part of another block 069 log.error("BlockPathEditFrame for OPath {}, but it is not part of OBlock {}", path.getName(), block.getDisplayName()); 070 JmriJOptionPane.showMessageDialog(BlockPathEditFrame.this, 071 Bundle.getMessage("OBlockEditWrongPath", path.getName(), block.getDisplayName()), 072 Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE); 073 // cancel edit 074 closeFrame(); 075 return; 076 } else { 077 _path = path; 078 _tomodel = turnouttable.getModel(); 079 if (_tomodel != null) { // test uses a plain JTable without getRowCount() 080 log.debug("TurnoutModel.size = {}", _tomodel.getRowCount()); 081 } 082 } 083 } 084 // fill Portals combo 085 pm = InstanceManager.getDefault(PortalManager.class); 086 for (Portal pi : pm.getPortalSet()) { 087 if (pi.getFromBlock() == _block || pi.getToBlock() == _block) { // show only relevant Portals 088 fromPortalComboBox.addItem(pi.getName()); // in both combos 089 toPortalComboBox.addItem(pi.getName()); 090 } 091 } 092 layoutFrame(); 093 blockName.setText(_block.getDisplayName()); 094 if (_newPath) { 095 resetFrame(); 096 } else { 097 populateFrame(path); 098 } 099 addCloseListener(this); 100 } 101 102 public void layoutFrame() { 103 frame.addHelpMenu("package.jmri.jmrit.beantable.OBlockTable", true); 104 frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS)); 105 frame.setSize(400, 500); 106 107 JPanel p = new JPanel(); 108 p.setLayout(new BoxLayout(p, BoxLayout.PAGE_AXIS)); 109 110 JPanel configGrid = new JPanel(); 111 GridLayout layout = new GridLayout(4, 2, 10, 0); // (int rows, int cols, int hgap, int vgap) 112 configGrid.setLayout(layout); 113 114 // row 1 115 configGrid.add(blockLabel); 116 configGrid.add(blockName); 117 118 // row 2 119 configGrid.add(pathLabel); 120 JPanel p1 = new JPanel(); 121 p1.add(pathUserName); 122 configGrid.add(p1); 123 124 // row 3 125 configGrid.add(fromPortalLabel); 126 fromPortalComboBox.addActionListener(e -> { 127 if ((fromPortalComboBox.getItemCount() > 0) && (fromPortalComboBox.getSelectedItem() != null) && 128 (toPortalComboBox.getSelectedItem() != null) 129 && (fromPortalComboBox.getSelectedItem().equals(toPortalComboBox.getSelectedItem()))) { 130 log.debug("resetting ToPortal"); 131 toPortalComboBox.setSelectedIndex(0); // clear the other one 132 } 133 }); 134 configGrid.add(fromPortalComboBox); 135 136 // row 4 137 configGrid.add(toPortalLabel); 138 toPortalComboBox.addActionListener(e -> { 139 if ((fromPortalComboBox.getItemCount() > 0) && (fromPortalComboBox.getSelectedItem() != null) && 140 (toPortalComboBox.getSelectedItem() != null) 141 && (fromPortalComboBox.getSelectedItem().equals(toPortalComboBox.getSelectedItem()))) { 142 log.debug("resetting FromPortal"); 143 fromPortalComboBox.setSelectedIndex(0); // clear the other one 144 } 145 }); 146 configGrid.add(toPortalComboBox); 147 148 p.add(configGrid); 149 150 // Length 151 JPanel physical = new JPanel(); 152 // copied from beanedit, also in BlockPathEditFrame 153 lengthSpinner.setModel( 154 new SpinnerNumberModel(Float.valueOf(0f), Float.valueOf(0f), Float.valueOf(1000f), Float.valueOf(0.01f))); 155 lengthSpinner.setEditor(new JSpinner.NumberEditor(lengthSpinner, "###0.00")); 156 lengthSpinner.setPreferredSize(new JTextField(8).getPreferredSize()); 157 lengthSpinner.setValue(0f); // reset from possible previous use 158 159 ButtonGroup bg = new ButtonGroup(); 160 bg.add(inch); 161 bg.add(cm); 162 163 p1 = new JPanel(); 164 p1.add(inch); 165 p1.add(cm); 166 p1.setLayout(new BoxLayout(p1, BoxLayout.PAGE_AXIS)); 167 inch.setSelected(true); 168 inch.addActionListener(e -> { 169 cm.setSelected(!inch.isSelected()); 170 updateLength(); 171 }); 172 cm.addActionListener(e -> { 173 inch.setSelected(!cm.isSelected()); 174 updateLength(); 175 }); 176 physical.add(p1); 177 178 JPanel p2 = new JPanel(); 179 p2.add(lengthSpinner); 180 lengthSpinner.setToolTipText(Bundle.getMessage("LengthToolTip", Bundle.getMessage("Path"))); 181 physical.add(p2); 182 183 p.add(physical); 184 185 JPanel totbl = new JPanel(); 186 totbl.setLayout(new BorderLayout(10, 10)); 187 totbl.setBorder(BorderFactory.createLineBorder(Color.BLACK)); 188 totbl.add(_turnoutTablePane, BorderLayout.CENTER); 189 p.add(totbl); 190 191 p2 = new JPanel(); 192 statusBar.setFont(statusBar.getFont().deriveFont(0.9f * blockName.getFont().getSize())); // a bit smaller 193 statusBar.setForeground(Color.gray); 194 p2.add(statusBar); 195 p.add(p2); 196 197 p.add(Box.createVerticalGlue()); 198 199 p2 = new JPanel(); 200 p2.setLayout(new BoxLayout(p2, BoxLayout.LINE_AXIS)); 201 JButton cancel; 202 p2.add(cancel = new JButton(Bundle.getMessage("ButtonCancel"))); 203 cancel.addActionListener((ActionEvent e) -> closeFrame()); 204 JButton ok; 205 p2.add(ok = new JButton(Bundle.getMessage("ButtonOK"))); 206 ok.addActionListener(this::okPressed); 207 p.add(p2, BorderLayout.SOUTH); 208 209 frame.getContentPane().add(p); 210 } 211 212 /** 213 * Populate the Edit OBlock frame with default values. 214 */ 215 public void resetFrame() { 216 pathUserName.setText(null); 217 if (toPortalComboBox.getItemCount() < 2) { 218 status(Bundle.getMessage("NotEnoughPortals"), true); 219 } else { 220 status(Bundle.getMessage("AddXStatusInitial1", Bundle.getMessage("Path"), Bundle.getMessage("ButtonCreate")), 221 false); // I18N to include original button name in help string 222 } 223 lengthSpinner.setValue(0f); 224 _newPath = true; 225 } 226 227 /** 228 * Populate the Edit Path frame with current values. 229 * 230 * @param p existing OPath to copy the attributes from 231 */ 232 public void populateFrame(OPath p) { 233 if (p == null) { 234 throw new IllegalArgumentException("Null OPath object"); 235 } 236 pathUserName.setText(p.getName()); 237 if (p.getFromPortal() != null) { 238 log.debug("BPEF FROMPORTAL name = {}", p.getFromPortal().getName()); 239 fromPortalComboBox.setSelectedItem(p.getFromPortal().getName()); 240 } 241 if (p.getToPortal() != null) { 242 log.debug("BPEF TOPORTAL name = {}", p.getToPortal().getName()); 243 toPortalComboBox.setSelectedItem(p.getToPortal().getName()); 244 } 245 if (_block.isMetric()) { 246 cm.setSelected(true); 247 lengthSpinner.setValue(_path.getLengthCm()); 248 } else { 249 inch.setSelected(true); // set first while length = 0 to prevent recalc 250 lengthSpinner.setValue(_path.getLengthIn()); 251 } 252 status(Bundle.getMessage("AddXStatusInitial3", Bundle.getMessage("Path"), Bundle.getMessage("ButtonOK")), false); 253 _newPath = false; 254 } 255 256 protected void okPressed(ActionEvent e) { 257 String user = pathUserName.getText().trim(); 258 if (user.equals("") || (_newPath && _block.getPathByName(user) != null)) { // check existing names before creating 259 status(user.equals("") ? Bundle.getMessage("WarningSysNameEmpty") : Bundle.getMessage("DuplPathName", user), true); 260 pathUserName.setBackground(Color.red); 261 log.debug("username empty"); 262 return; 263 } 264 if (_newPath) { 265 Portal fromPortal = _block.getPortalByName((String) fromPortalComboBox.getSelectedItem()); 266 Portal toPortal = _block.getPortalByName((String) toPortalComboBox.getSelectedItem()); 267 if (fromPortal != null || toPortal != null) { 268 _path = new OPath(user, _block, fromPortal, toPortal, null); 269 if (!_block.addPath(_path)) { 270 status(Bundle.getMessage("AddPathFailed", user), true); 271 } else { 272 _pathmodel.initTempRow(); 273 _core.updateOBlockTablesMenu(); 274 _pathmodel.fireTableDataChanged(); 275 closeFrame(); // success 276 } 277 } else { 278 log.debug("_newPath - could not get from/to Portal from this OBlock"); 279 } 280 } else if (!_path.getName().equals(user)) { 281 _path.setName(user); // name change on existing path 282 } 283 try { // adapted from BlockPathTableModel setValue 284 // set fromPortal 285 if (!setPortal(fromPortalComboBox, toPortalComboBox, true)) { 286 return; 287 } 288 // set toPortal 289 if (!setPortal(toPortalComboBox, fromPortalComboBox, false)) { 290 return; 291 } 292 _path.setLength((float) lengthSpinner.getValue() * (cm.isSelected() ? 10.0f : 25.4f)); // stored in mm 293 _block.setMetricUnits(cm.isSelected()); 294 } catch (IllegalArgumentException ex) { 295 JmriJOptionPane.showMessageDialog(this, ex.getMessage(), 296 Bundle.getMessage("PathCreateErrorTitle"), JmriJOptionPane.ERROR_MESSAGE); 297 status(Bundle.getMessage("AddPathFailed", user), true); 298 return; 299 } 300 // Notify changes 301 if (_pathmodel != null) { 302 _pathmodel.fireTableDataChanged(); 303 } 304 _core.setPathEdit(false); 305 log.debug("BlockPathEditFrame.okPressed complete. pathEdit = false"); 306 closeFrame(); 307 } 308 309 private boolean setPortal(JComboBox<String> portalBox, JComboBox<String> compareBox, boolean isFrom) { 310 if (portalBox.getSelectedIndex() <= 0) { 311 // 0 = empty choice, need at least 1 Portal in an OBlockPath 312 log.debug("fromPortal no selection, require 1 so check toPortal is not empty"); 313 if (compareBox.getSelectedIndex() > 0) { 314 if (isFrom) { 315 _path.setFromPortal(null); // portal can be removed from path by setting to null but we require at least 1 316 } else { 317 _path.setToPortal(null); // at least 1 318 } 319 log.debug("removed {}Portal", (isFrom ? "From" : "To")); 320 } else { 321 status(Bundle.getMessage("WarnPortalOnPath"), true); 322 return false; 323 } 324 } else { 325 String portalName = (String) portalBox.getSelectedItem(); 326 log.debug("looking for {}Portal {} (combo item {})", (isFrom ? "From" : "To"), portalName, portalBox.getSelectedIndex()); 327 Portal portal = _block.getPortalByName(portalName); 328 if (portal == null || pm.getPortal(portalName) == null) { 329 int val = _core.verifyWarning(Bundle.getMessage("BlockPortalConflict", portalName, _block.getDisplayName())); 330 if (val == 2) { 331 return false; // abort 332 } 333 portal = pm.providePortal(portalName); 334 if (isFrom) { 335 if (!portal.setFromBlock(_block, false)) { 336 val = _core.verifyWarning(Bundle.getMessage("BlockPathsConflict", portalName, portal.getFromBlockName())); 337 } 338 } else { 339 if (!portal.setToBlock(_block, false)) { 340 val = _core.verifyWarning(Bundle.getMessage("BlockPathsConflict", portalName, portal.getToBlockName())); 341 } 342 } 343 if (val == 2) { 344 return false; 345 } 346 log.debug("fromPortal == null"); 347 portal.setFromBlock(_block, true); 348 } 349 if (isFrom) { 350 _path.setFromPortal(portal); // portal can be removed from path by setting to null but we require at least 1 351 } else { 352 _path.setToPortal(portal); // at least 1 353 } 354 if (!portal.addPath(_path)) { 355 status(Bundle.getMessage("AddPathFailed", portalName), true); 356 return false ; 357 } 358 } 359 return true; 360 } 361 362 protected void closeFrame() { 363 // remind to save, if Path was created or edited 364 if (isDirty) { 365 showReminderMessage(); 366 isDirty = false; 367 } 368 // hide addFrame 369 setVisible(false); 370 371 if (_tomodel != null) { 372 _tomodel.dispose(); 373 } 374 _core.setPathEdit(false); 375 log.debug("BlockPathEditFrame.closeFrame pathEdit=False"); 376 this.dispose(); 377 } 378 379 protected void showReminderMessage() { 380 if (checkEnabled) return; 381 InstanceManager.getDefault(UserPreferencesManager.class). 382 showInfoMessage(Bundle.getMessage("ReminderTitle"), // NOI18N 383 Bundle.getMessage("ReminderSaveString", Bundle.getMessage("MenuItemOBlockTable")), // NOI18N 384 "BlockPathEditFrame", "remindSaveOBlock"); // NOI18N 385 } 386 387 // copied from beanedit, also used in BlockPathEditFrame 388 private void updateLength() { 389 float len = (float) lengthSpinner.getValue(); 390 if (inch.isSelected()) { 391 lengthSpinner.setValue(len/2.54f); 392 } else { 393 lengthSpinner.setValue(len*2.54f); 394 } 395 } 396 397 void status(String message, boolean warn){ 398 statusBar.setText(message); 399 statusBar.setForeground(warn ? Color.red : Color.gray); 400 } 401 402 // listen for frame closing 403 void addCloseListener(JmriJFrame frame) { 404 frame.addWindowListener(new java.awt.event.WindowAdapter() { 405 @Override 406 public void windowClosing(java.awt.event.WindowEvent e) { 407 _core.setPathEdit(false); 408 log.debug("BlockPathEditFrame.closeFrame pathEdit=False"); 409 frame.dispose(); 410 } 411 }); 412 } 413 414 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockPathEditFrame.class); 415 416}