001package jmri.jmrit.signalling; 002 003import java.awt.BorderLayout; 004import java.awt.event.ActionEvent; 005import java.awt.event.ActionListener; 006import java.beans.PropertyChangeListener; 007import java.util.List; 008import java.util.Set; 009 010import javax.swing.BorderFactory; 011import javax.swing.BoxLayout; 012import javax.swing.JButton; 013import javax.swing.JLabel; 014import javax.swing.JPanel; 015import javax.swing.JScrollPane; 016import javax.swing.JTable; 017import javax.swing.JTextField; 018import javax.swing.SortOrder; 019import javax.swing.table.AbstractTableModel; 020import javax.swing.table.TableCellEditor; 021import javax.swing.table.TableRowSorter; 022 023import jmri.InstanceManager; 024import jmri.SignalMast; 025import jmri.SignalMastLogic; 026import jmri.SignalMastLogicManager; 027import jmri.jmrit.display.EditorManager; 028import jmri.jmrit.display.layoutEditor.LayoutBlockManager; 029import jmri.jmrit.display.layoutEditor.LayoutEditor; 030import jmri.swing.RowSorterUtil; 031import jmri.util.JmriJFrame; 032import jmri.util.swing.JmriJOptionPane; 033import jmri.util.table.ButtonEditor; 034import jmri.util.table.ButtonRenderer; 035 036/** 037 * Frame for the Signal Mast Table - Edit Logic Pane. 038 * 039 * @author Kevin Dickerson Copyright (C) 2011 040 * @author Egbert Broerse Copyright (C) 2017 041 */ 042public class SignallingSourcePanel extends jmri.util.swing.JmriPanel implements PropertyChangeListener { 043 044 SignalMastLogic sml; 045 SignalMast sourceMast; 046 JLabel fixedSourceMastLabel = new JLabel(); 047 048 JButton discoverPairs = new JButton(Bundle.getMessage("ButtonDiscover")); // NOI18N 049 050 SignalMastAspectModel _AppearanceModel; 051 JScrollPane _SignalAppearanceScrollPane; 052 053 /** 054 * Create a Signalling Source configuration Pane showing a list of defined 055 * destination masts and allowing creation of new source-destination pairs 056 * as well as showing a button to start Autodetect configuration. 057 * @param sourceMast The source mast for this SML Source Pairs pane 058 */ 059 public SignallingSourcePanel(final SignalMast sourceMast) { 060 super(); 061 sml = InstanceManager.getDefault(SignalMastLogicManager.class).getSignalMastLogic(sourceMast); 062 this.sourceMast = sourceMast; 063 fixedSourceMastLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("SourceMast")) + " " + sourceMast.getDisplayName()); // NOI18N 064 if (sml != null) { 065 _signalMastList = sml.getDestinationList(); 066 } 067 initGui(); 068 } 069 070 @Override 071 public void initComponents() { 072 InstanceManager.getDefault(LayoutBlockManager.class).addPropertyChangeListener(this); 073 InstanceManager.getDefault(SignalMastLogicManager.class).addPropertyChangeListener(this); 074 } 075 076 private void initGui() { 077 setLayout(new BorderLayout()); 078 079 JPanel header = new JPanel(); 080 header.setLayout(new BoxLayout(header, BoxLayout.Y_AXIS)); 081 082 JPanel sourcePanel = new JPanel(); 083 sourcePanel.add(fixedSourceMastLabel); 084 header.add(sourcePanel); 085 add(header, BorderLayout.NORTH); 086 087 _AppearanceModel = new SignalMastAspectModel(); 088 JTable table = new JTable(_AppearanceModel); 089 TableRowSorter<SignalMastAspectModel> sorter = new TableRowSorter<>(_AppearanceModel); 090 RowSorterUtil.setSortOrder(sorter, SignalMastAspectModel.SYSNAME_COLUMN, SortOrder.ASCENDING); 091 table.setRowSorter(sorter); 092 table.setRowSelectionAllowed(false); 093 table.setPreferredScrollableViewportSize(new java.awt.Dimension(600, 120)); 094 _AppearanceModel.configureTable(table); 095 _SignalAppearanceScrollPane = new JScrollPane(table); 096 _AppearanceModel.fireTableDataChanged(); 097 add(_SignalAppearanceScrollPane, BorderLayout.CENTER); 098 099 JPanel footer = new JPanel(); 100 101 footer.add(discoverPairs); 102 discoverPairs.addActionListener(this::discoverPressed); 103 104 JButton addLogic = new JButton(Bundle.getMessage("AddLogic")); // NOI18N 105 footer.add(addLogic); 106 addLogic.addActionListener(new ActionListener() { 107 @Override 108 public void actionPerformed(ActionEvent e) { 109 class WindowMaker implements Runnable { 110 111 WindowMaker() { 112 } 113 114 @Override 115 public void run() { 116 SignallingAction sigLog = new SignallingAction(); // opens a frame, opens a panel in that frame 117 sigLog.setMast(sourceMast, null); 118 sigLog.actionPerformed(null); 119 // unable to receive changes directly from created panel, so listen to common SML ancestor 120 } 121 } 122 WindowMaker t = new WindowMaker(); 123 javax.swing.SwingUtilities.invokeLater(t); 124 } 125 }); 126 127 add(footer, BorderLayout.SOUTH); 128 } 129 130 /** 131 * Remove references to and from this object, so that it can eventually be 132 * garbage-collected. 133 */ 134 @Override 135 public void dispose() { 136 InstanceManager.getDefault(LayoutBlockManager.class).removePropertyChangeListener(this); 137 InstanceManager.getDefault(SignalMastLogicManager.class).removePropertyChangeListener(this); 138 super.dispose(); 139 } 140 141 JmriJFrame signalMastLogicFrame = null; 142 JLabel sourceLabel = new JLabel(); 143 144 /** 145 * Respond to the Discover button being pressed. 146 * Check whether AdvancedRouting is turned on and any Layout Editor Panels 147 * are present. For each LE Panel, call discoverSignallingDest() 148 * {@link jmri.SignalMastLogicManager#discoverSignallingDest(SignalMast, LayoutEditor)} 149 * @param e The button event 150 */ 151 void discoverPressed(ActionEvent e) { 152 if (!InstanceManager.getDefault(LayoutBlockManager.class).isAdvancedRoutingEnabled()) { 153 int response = JmriJOptionPane.showConfirmDialog(this, Bundle.getMessage("EnableLayoutBlockRouting"), 154 Bundle.getMessage("QuestionTitle"), JmriJOptionPane.YES_NO_OPTION); 155 if ( response == JmriJOptionPane.YES_OPTION ) { 156 InstanceManager.getDefault(LayoutBlockManager.class).enableAdvancedRouting(true); 157 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("LayoutBlockRoutingEnabledShort")); // NOI18N 158 } 159 } 160 161 Set<LayoutEditor> layout = InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class); 162 if (!layout.isEmpty()) { 163 signalMastLogicFrame = new JmriJFrame(Bundle.getMessage("DiscoverMastsTitle"), false, false); // NOI18N 164 signalMastLogicFrame.setPreferredSize(null); 165 JPanel panel1 = new JPanel(); 166 sourceLabel = new JLabel(Bundle.getMessage("DiscoveringMasts")); // NOI18N 167 sourceLabel.setBorder(BorderFactory.createEmptyBorder(5, 20, 5, 20)); 168 panel1.add(sourceLabel); 169 signalMastLogicFrame.add(sourceLabel); 170 signalMastLogicFrame.pack(); 171 signalMastLogicFrame.setVisible(true); 172 173 for (LayoutEditor editor : layout) { 174 try { 175 InstanceManager.getDefault(SignalMastLogicManager.class).discoverSignallingDest(sourceMast, editor); 176 } catch (jmri.JmriException ex) { 177 signalMastLogicFrame.setVisible(false); 178 JmriJOptionPane.showMessageDialog(this, ex.toString()); 179 } 180 } 181 signalMastLogicFrame.setVisible(false); 182 signalMastLogicFrame.dispose(); 183 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("GenComplete")); // NOI18N 184 } else { 185 // don't take the trouble of searching 186 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("GenSkipped")); // NOI18N 187 } 188 } 189 190 /** 191 * Listen for property changes in the SML that's being configured. 192 * @param e The button event 193 */ 194 @Override 195 public void propertyChange(java.beans.PropertyChangeEvent e) { 196 if ( SignalMastLogicManager.PROPERTY_AUTO_SIGNALMAST_GENERATE_COMPLETE.equals(e.getPropertyName())) { 197 if (sml == null) { 198 updateDetails(); 199 } 200 log.debug("Generate complete for a LE panel ({}): mast = {}", this.hashCode(), sourceMast); 201 } 202 if ( LayoutBlockManager.PROPERTY_ADVANCED_ROUTING_ENABLED.equals(e.getPropertyName())) { 203 boolean newValue = (Boolean) e.getNewValue(); 204 discoverPairs.setEnabled(newValue); 205 } 206 log.debug("SSP 173 Event: {}; Source: {}", e.getPropertyName(), e); // doesn't get notified, newDestination 207 if ( jmri.Manager.PROPERTY_LENGTH.equals(e.getPropertyName())) { // redraw the Pairs table 208 updateDetails(); 209 } 210 } 211 212 private List<SignalMast> _signalMastList; 213 214 /** 215 * Refresh the list of destination Signal Masts available for edit in the current SML. 216 */ 217 private void updateDetails() { 218 SignalMastLogic old = sml; 219 sml = InstanceManager.getDefault(SignalMastLogicManager.class).getSignalMastLogic(sourceMast); 220 if (sml != null) { 221 _signalMastList = sml.getDestinationList(); 222 _AppearanceModel.updateSignalMastLogic(old, sml); 223 } 224 } 225 226 /** 227 * TableModel to store SML control Signal Masts and their Set To Aspect. 228 */ 229 public class SignalMastAspectModel extends AbstractTableModel implements PropertyChangeListener { 230 231 SignalMastAspectModel() { 232 super(); 233 if (sml != null) { 234 sml.addPropertyChangeListener(this); // pick up creation of a new pair in the sml 235 } 236 } 237 238 void updateSignalMastLogic(SignalMastLogic smlOld, SignalMastLogic smlNew) { 239 if (smlOld != null) { 240 smlOld.removePropertyChangeListener(this); 241 } 242 if (smlNew != null) { 243 smlNew.addPropertyChangeListener(this); 244 } 245 fireTableDataChanged(); 246 } 247 248 @Override 249 public Class<?> getColumnClass(int c) { 250 if (c == ACTIVE_COLUMN) { 251 return Boolean.class; 252 } 253 if (c == ENABLE_COLUMN) { 254 return Boolean.class; 255 } 256 if (c == EDIT_COLUMN) { 257 return JButton.class; 258 } 259 if (c == DEL_COLUMN) { 260 return JButton.class; 261 } 262 return String.class; 263 } 264 265 public void configureTable(JTable table) { 266 // allow reordering of the columns 267 table.getTableHeader().setReorderingAllowed(true); 268 269 // have to shut off autoResizeMode to get horizontal scroll to work (JavaSwing p 541) 270 table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 271 272 // resize columns as requested 273 for (int i = 0; i < table.getColumnCount(); i++) { 274 int width = getPreferredWidth(i); 275 table.getColumnModel().getColumn(i).setPreferredWidth(width); 276 } 277 table.sizeColumnsToFit(-1); 278 279 configEditColumn(table); 280 } 281 282 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "DB_DUPLICATE_SWITCH_CLAUSES", 283 justification="better to keep cases in column order rather than to combine") 284 public int getPreferredWidth(int col) { 285 switch (col) { 286 case SYSNAME_COLUMN: 287 return new JTextField(15).getPreferredSize().width; 288 case ENABLE_COLUMN: 289 case ACTIVE_COLUMN: 290 return new JTextField(5).getPreferredSize().width; 291 case USERNAME_COLUMN: 292 return new JTextField(15).getPreferredSize().width; 293 case EDIT_COLUMN: // not actually used due to the configureTable, setColumnToHoldButton, configureButton 294 return new JTextField(22).getPreferredSize().width; 295 case DEL_COLUMN: // not actually used due to the configureTable, setColumnToHoldButton, configureButton 296 return new JTextField(22).getPreferredSize().width; 297 default: 298 log.warn("Unexpected column in getPreferredWidth: {}", col); // NOI18N 299 return new JTextField(8).getPreferredSize().width; 300 } 301 } 302 303 @Override 304 public String getColumnName(int col) { 305 if (col == USERNAME_COLUMN) { 306 return Bundle.getMessage("ColumnUserName"); // NOI18N 307 } 308 if (col == SYSNAME_COLUMN) { 309 return Bundle.getMessage("DestMast"); // NOI18N 310 } 311 if (col == ACTIVE_COLUMN) { 312 return Bundle.getMessage("SensorStateActive"); // "Active" // NOI18N 313 } 314 if (col == ENABLE_COLUMN) { 315 return Bundle.getMessage("ColumnHeadEnabled"); // NOI18N 316 } 317 if (col == EDIT_COLUMN) { 318 return ""; //no title above Edit buttons 319 } 320 if (col == DEL_COLUMN) { 321 return ""; //no title above Delete buttons 322 } 323 return ""; 324 } 325 326 /** 327 * Remove references to and from this object, so that it can eventually be 328 * garbage-collected. 329 */ 330 public void dispose() { 331 if (sml != null) { 332 sml.removePropertyChangeListener(this); 333 } 334 } 335 336 /** 337 * Listen for changes to specific properties of the displayed Signal Masts. 338 * @param e The ChangeEvent heard 339 */ 340 @Override 341 public void propertyChange(java.beans.PropertyChangeEvent e) { 342 switch (e.getPropertyName()) { 343 case SignalMastLogic.PROPERTY_LENGTH: 344 // should pick up adding a new destination mast, but doesn't refresh table by itself 345 _signalMastList = sml.getDestinationList(); 346 int length = (Integer) e.getNewValue(); 347 if (length == 0) { 348 sml.removePropertyChangeListener(this); 349 sml = null; 350 } 351 fireTableDataChanged(); 352 break; 353 case SignalMastLogic.PROPERTY_UPDATED_DESTINATION: 354 // a new NamedBean is available in the manager 355 _signalMastList = sml.getDestinationList(); 356 fireTableDataChanged(); 357 break; 358 case SignalMastLogic.PROPERTY_STATE: 359 case SignalMastLogic.PROPERTY_ENABLED: 360 fireTableDataChanged(); 361 fireTableRowsUpdated(0, _signalMastList.size()-1); 362 break; 363 default: 364 break; 365 } 366 log.debug("SSP 310 Event: {}", e.getPropertyName()); // NOI18N 367 } 368 369 /** 370 * Display buttons in 2 columns of the manual control signal masts table. 371 * @param table The control signal mast table to be configured 372 */ 373 protected void configEditColumn(JTable table) { 374 // have the Delete column hold a button 375 setColumnToHoldButton(table, EDIT_COLUMN, 376 new JButton(Bundle.getMessage("ButtonEdit"))); // NOI18N 377 setColumnToHoldButton(table, DEL_COLUMN, 378 new JButton(Bundle.getMessage("ButtonDelete"))); // NOI18N 379 } 380 381 /** 382 * Helper function for {@link #configEditColumn(JTable)}. 383 * @param table The control signal mast table to be configured 384 * @param column Index for the column to put the button in 385 * @param sample JButton to put there 386 */ 387 protected void setColumnToHoldButton(JTable table, int column, JButton sample) { 388 // install a button renderer & editor 389 ButtonRenderer buttonRenderer = new ButtonRenderer(); 390 table.setDefaultRenderer(JButton.class, buttonRenderer); 391 TableCellEditor buttonEditor = new ButtonEditor(new JButton()); 392 table.setDefaultEditor(JButton.class, buttonEditor); 393 // ensure the table rows, columns have enough room for buttons 394 table.setRowHeight(sample.getPreferredSize().height); 395 table.getColumnModel().getColumn(column) 396 .setPreferredWidth((sample.getPreferredSize().width) + 4); 397 } 398 399 /** 400 * Get the number of columns in the signal masts table. 401 * @return Fixed value of 6 402 */ 403 @Override 404 public int getColumnCount() { 405 return 6; 406 } 407 408 /** 409 * Query whether the cells in a table column should respond to clicks. 410 * @param r Index for the cell row 411 * @param c Index for the cell column 412 */ 413 @Override 414 public boolean isCellEditable(int r, int c) { 415 if (c == EDIT_COLUMN) { 416 return true; 417 } 418 if (c == DEL_COLUMN) { 419 return true; 420 } 421 if (c == ENABLE_COLUMN) { 422 return true; 423 } 424 return ((c == USERNAME_COLUMN)); 425 } 426 427 /** 428 * Respond to the Edit Logic button being clicked. 429 * @param r Index for the cell row 430 */ 431 protected void editPair(int r) { 432 433 class WindowMaker implements Runnable { 434 435 int row; 436 437 WindowMaker(int r) { 438 row = r; 439 } 440 441 @Override 442 public void run() { 443 log.debug("SML Edit existing logic started"); // NOI18N 444 SignallingAction sigLog = new SignallingAction(); 445 sigLog.setMast(sourceMast, _signalMastList.get(row)); 446 sigLog.actionPerformed(null); 447 // Note: we cannot tell if Edit pair was cancelled 448 } 449 } 450 WindowMaker t = new WindowMaker(r); 451 javax.swing.SwingUtilities.invokeLater(t); 452 } 453 454 protected void deletePair(int r) { 455 InstanceManager.getDefault(jmri.SignalMastLogicManager.class).removeSignalMastLogic(sml, _signalMastList.get(r)); 456 } 457 458 public static final int SYSNAME_COLUMN = 0; 459 public static final int USERNAME_COLUMN = 1; 460 public static final int ACTIVE_COLUMN = 2; 461 public static final int ENABLE_COLUMN = 3; 462 public static final int EDIT_COLUMN = 4; 463 public static final int DEL_COLUMN = 5; 464 465 public void setSetToState(String x) { 466 } 467 468 /** 469 * Get the number of Included signal masts for this SML. 470 */ 471 @Override 472 public int getRowCount() { 473 if (_signalMastList == null) { 474 return 0; 475 } 476 return _signalMastList.size(); 477 } 478 479 /** 480 * Retrieve the contents to display in a cell in the table, in terms of model 481 * @param r index for the cell row 482 * @param c index for the cell column 483 * @return The value (text) stored in the cell 484 */ 485 @Override 486 public Object getValueAt(int r, int c) { 487 if (sml == null) { 488 return null; 489 } 490 // some error checking 491 if (r >= _signalMastList.size()) { 492 log.debug("row is greater than turnout list size"); // NOI18N 493 return null; 494 } 495 switch (c) { 496 case USERNAME_COLUMN: 497 return _signalMastList.get(r).getUserName(); 498 case SYSNAME_COLUMN: // slot number 499 return _signalMastList.get(r).getSystemName(); 500 case ACTIVE_COLUMN: 501 return sml.isActive(_signalMastList.get(r)); 502 case ENABLE_COLUMN: 503 return sml.isEnabled(_signalMastList.get(r)); 504 case EDIT_COLUMN: 505 return Bundle.getMessage("ButtonEdit"); // NOI18N 506 case DEL_COLUMN: 507 return Bundle.getMessage("ButtonDelete"); // NOI18N 508 default: 509 return null; 510 } 511 } 512 513 /** 514 * Process the contents from a table cell, in terms of model 515 * @param type the object type of the cell contents 516 * @param r index for the cell row 517 * @param c index for the cell column 518 */ 519 @Override 520 public void setValueAt(Object type, int r, int c) { 521 if (c == EDIT_COLUMN) { 522 editPair(r); 523 } else if (c == DEL_COLUMN) { 524 deletePair(r); 525 } else if (c == ENABLE_COLUMN) { 526 boolean b = ((Boolean) type).booleanValue(); 527 if (b) { 528 sml.setEnabled(_signalMastList.get(r)); 529 } else { 530 sml.setDisabled(_signalMastList.get(r)); 531 } 532 533 } 534 } 535 } 536 537 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignallingSourcePanel.class); 538 539}