001package jmri.jmrit.logixng.tools.swing; 002 003import java.awt.Component; 004import java.awt.Container; 005import java.awt.Dimension; 006import java.awt.FlowLayout; 007import java.awt.Toolkit; 008import java.awt.datatransfer.StringSelection; 009import java.awt.event.ActionEvent; 010import java.io.IOException; 011import java.util.ArrayList; 012import java.util.EventListener; 013import java.util.HashMap; 014import java.util.List; 015 016import javax.swing.*; 017import javax.swing.event.ListSelectionListener; 018import javax.swing.table.AbstractTableModel; 019import javax.swing.table.JTableHeader; 020 021import jmri.InstanceManager; 022import jmri.jmrit.beantable.BeanTableDataModel; 023import jmri.jmrit.logixng.*; 024import jmri.jmrit.logixng.implementation.*; 025import jmri.jmrit.logixng.util.ReferenceUtil; 026import jmri.util.swing.JmriJOptionPane; 027import jmri.util.JmriJFrame; 028 029/** 030 * Editor for LogixNG Tables 031 * 032 * @author Dave Duchamp Copyright (C) 2007 (ConditionalListEdit) 033 * @author Pete Cressman Copyright (C) 2009, 2010, 2011 (ConditionalListEdit) 034 * @author Matthew Harris copyright (c) 2009 (ConditionalListEdit) 035 * @author Dave Sand copyright (c) 2017 (ConditionalListEdit) 036 * @author Daniel Bergqvist (c) 2019 037 * @author J. Scott Walton (c) 2022 (Csv Types) 038 */ 039 public final class TableEditor implements AbstractLogixNGEditor<NamedTable> { 040 041 private NamedTableManager _tableManager = null; 042 private NamedTable _curTable = null; 043 044 private boolean _inEditMode = false; 045 046 private boolean _showReminder = false; 047 private boolean _checkEnabled = jmri.InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).isStoreCheckEnabled(); 048 049 private final SymbolTable symbolTable = new DefaultSymbolTable(); 050 051 /** 052 * Create a new ConditionalNG List View editor. 053 * 054 * @param m the bean table model 055 * @param sName name of the NamedTable being edited 056 */ 057 public TableEditor(BeanTableDataModel<NamedTable> m, String sName) { 058 _tableManager = InstanceManager.getDefault(jmri.jmrit.logixng.NamedTableManager.class); 059 _curTable = _tableManager.getBySystemName(sName); 060 makeEditTableWindow(); 061 } 062 063 // ------------ NamedTable Variables ------------ 064 private JmriJFrame _editLogixNGFrame = null; 065 private final JTextField editUserName = new JTextField(20); 066 private final JTextField editCsvTableName = new JTextField(40); 067 068 // ------------ ConditionalNG Variables ------------ 069 private TableTableModel tableTableModel = null; 070 071 /** 072 * Create and/or initialize the Edit NamedTable pane. 073 */ 074 private void makeEditTableWindow() { 075 editUserName.setText(_curTable.getUserName()); 076 // clear conditional table if needed 077 if (tableTableModel != null) { 078 tableTableModel.fireTableStructureChanged(); 079 } 080 _inEditMode = true; 081 if (_editLogixNGFrame == null) { 082 if (_curTable.getUserName() != null) { 083 _editLogixNGFrame = new JmriJFrame( 084 Bundle.getMessage("TitleEditLogixNG2", 085 _curTable.getSystemName(), // NOI18N 086 _curTable.getUserName()), // NOI18N 087 false, 088 false); 089 } else { 090 _editLogixNGFrame = new JmriJFrame( 091 Bundle.getMessage("TitleEditLogixNG", _curTable.getSystemName()), // NOI18N 092 false, 093 false); 094 } 095 _editLogixNGFrame.addHelpMenu( 096 "package.jmri.jmrit.logixng.LogixNGTableTableEditor", true); // NOI18N 097 _editLogixNGFrame.setLocation(100, 30); 098 Container contentPane = _editLogixNGFrame.getContentPane(); 099 contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS)); 100 JPanel panel1 = new JPanel(); 101 panel1.setLayout(new FlowLayout()); 102 JLabel systemNameLabel = new JLabel(Bundle.getMessage("ColumnSystemName") + ":"); // NOI18N 103 panel1.add(systemNameLabel); 104 JLabel fixedSystemName = new JLabel(_curTable.getSystemName()); 105 panel1.add(fixedSystemName); 106 contentPane.add(panel1); 107 JPanel panel2 = new JPanel(); 108 panel2.setLayout(new FlowLayout()); 109 JLabel userNameLabel = new JLabel(Bundle.getMessage("ColumnUserName") + ":"); // NOI18N 110 panel2.add(userNameLabel); 111 panel2.add(editUserName); 112 editUserName.setToolTipText(Bundle.getMessage("LogixNGUserNameHint2")); // NOI18N 113 contentPane.add(panel2); 114 115 boolean isCsvTable = _curTable instanceof DefaultCsvNamedTable; 116 117 JPanel panel3 = new JPanel(); 118 panel3.setLayout(new FlowLayout()); 119 JLabel tableTypeLabel = new JLabel(Bundle.getMessage("TableEditor_TableType") + ": "); // NOI18N 120 panel3.add(tableTypeLabel); 121 panel3.add(new JLabel( 122 isCsvTable 123 ? Bundle.getMessage("TableEditor_CsvFile") 124 : Bundle.getMessage("TableEditor_UnknownTableType"))); 125 contentPane.add(panel3); 126 127 if (isCsvTable) { 128 JPanel csvTypePanel = new JPanel(); 129 csvTypePanel.setLayout(new FlowLayout()); 130 csvTypePanel.add(new JLabel(Bundle.getMessage("TableEditor_Csv_Type") + ":")); 131 JLabel csvTypeLabel = new JLabel(); 132 Table.CsvType csvType = ((DefaultCsvNamedTable) _curTable).getCsvType(); 133 if (csvType == null || csvType.equals(Table.CsvType.TABBED)) { 134 csvTypeLabel.setText(Table.CsvType.TABBED.toString()); 135 } else if (csvType.equals(Table.CsvType.COMMA)) { 136 csvTypeLabel.setText(Table.CsvType.COMMA.toString()); 137 } else { 138 throw new RuntimeException("unrecognized csvType"); 139 } 140 141 csvTypePanel.add(csvTypeLabel); 142 contentPane.add(csvTypePanel); 143 JPanel panel4 = new JPanel(); 144 panel4.setLayout(new FlowLayout()); 145 JLabel tableFileNameLabel = new JLabel(Bundle.getMessage("TableEditor_FileName") + ": "); // NOI18N 146 panel4.add(tableFileNameLabel); 147 editCsvTableName.setText(((DefaultCsvNamedTable)_curTable).getFileName()); 148 editCsvTableName.setEditable(false); 149 panel4.add(editCsvTableName); 150 contentPane.add(panel4); 151 } 152 153 154 // add table of Tables 155 JPanel pctSpace = new JPanel(); 156 pctSpace.setLayout(new FlowLayout()); 157 pctSpace.add(new JLabel(" ")); 158 contentPane.add(pctSpace); 159 JPanel pTitle = new JPanel(); 160 pTitle.setLayout(new FlowLayout()); 161 contentPane.add(pTitle); 162 // initialize table of conditionals 163 tableTableModel = new TableTableModel(); 164 JTable tableTable = new JTable(tableTableModel); 165 tableTable.setCellSelectionEnabled(true); 166 tableTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 167 tableTable.setAutoResizeMode( JTable.AUTO_RESIZE_OFF ); 168 tableTable.getTableHeader().setReorderingAllowed(false); 169 170 JButton cellRefByIndexButton = new JButton(Bundle.getMessage("TableEditor_CopyToClipboard")); // NOI18N 171 JLabel cellRefByIndexLabel = new JLabel(); // NOI18N 172 JTextField cellRefByIndex = new JTextField(); 173 cellRefByIndex.setEditable(false); 174 cellRefByIndexButton.setEnabled(false); 175 176 JButton cellRefByHeaderButton = new JButton(Bundle.getMessage("TableEditor_CopyToClipboard")); // NOI18N 177 JLabel cellRefByHeaderLabel = new JLabel(); // NOI18N 178 JTextField cellRefByHeader = new JTextField(); 179 cellRefByHeader.setEditable(false); 180 cellRefByHeaderButton.setEnabled(false); 181 182 java.awt.datatransfer.Clipboard clipboard = 183 Toolkit.getDefaultToolkit().getSystemClipboard(); 184 185 cellRefByIndexButton.addActionListener( 186 (evt) -> { clipboard.setContents(new StringSelection(cellRefByIndexLabel.getText()), null);}); 187 188 cellRefByHeaderButton.addActionListener( 189 (evt) -> { clipboard.setContents(new StringSelection(cellRefByHeaderLabel.getText()), null);}); 190 191 ListSelectionListener selectCellListener = (evt) -> { 192 String refByIndex = String.format("{%s[%d,%d]}", _curTable.getDisplayName(), tableTable.getSelectedRow()+1, tableTable.getSelectedColumn()+1); 193 cellRefByIndexLabel.setText(refByIndex); // NOI18N 194 cellRefByIndex.setText(ReferenceUtil.getReference(symbolTable, refByIndex)); // NOI18N 195 cellRefByIndexButton.setEnabled(true); 196 197 Object rowHeaderObj = _curTable.getCell(tableTable.getSelectedRow()+1, 0); 198 Object columnHeaderObj = _curTable.getCell(0, tableTable.getSelectedColumn()+1); 199 String rowHeader = rowHeaderObj != null ? rowHeaderObj.toString() : ""; 200 String columnHeader = columnHeaderObj != null ? columnHeaderObj.toString() : ""; 201 if (!rowHeader.isEmpty() && !columnHeader.isEmpty()) { 202 cellRefByHeaderButton.setEnabled(true); 203 String refByHeader = String.format("{%s[%s,%s]}", _curTable.getDisplayName(), _curTable.getCell(tableTable.getSelectedRow()+1,0), _curTable.getCell(0,tableTable.getSelectedColumn()+1)); 204 cellRefByHeaderLabel.setText(refByHeader); // NOI18N 205 cellRefByHeader.setText(ReferenceUtil.getReference(symbolTable, refByIndex)); // NOI18N 206 } else { 207 cellRefByHeaderButton.setEnabled(false); 208 cellRefByHeaderLabel.setText(""); // NOI18N 209 cellRefByHeader.setText(""); // NOI18N 210 } 211 }; 212 tableTable.getSelectionModel().addListSelectionListener(selectCellListener); 213 tableTable.getColumnModel().getSelectionModel().addListSelectionListener(selectCellListener); 214 215 ListModel<Object> lm = new RowHeaderListModel(); 216 217 JList<Object> rowHeader = new JList<>(lm); 218 rowHeader.setFixedCellHeight( 219 tableTable.getRowHeight() 220// tableTable.getRowHeight() + tableTable.getRowMargin() 221// + table.getIntercellSpacing().height 222 ); 223 rowHeader.setCellRenderer(new RowHeaderRenderer(tableTable)); 224 225 JScrollPane tableTableScrollPane = new JScrollPane(tableTable); 226 tableTableScrollPane.setRowHeaderView(rowHeader); 227 Dimension dim = tableTable.getPreferredSize(); 228 dim.height = 450; 229 tableTableScrollPane.getViewport().setPreferredSize(dim); 230 contentPane.add(tableTableScrollPane); 231 232 JPanel panel4 = new JPanel(); 233 panel4.setLayout(new FlowLayout()); 234 panel4.add(cellRefByIndexButton); 235 panel4.add(cellRefByIndexLabel); 236 panel4.add(cellRefByIndex); 237 contentPane.add(panel4); 238 239 JPanel panel5 = new JPanel(); 240 panel5.setLayout(new FlowLayout()); 241 panel5.add(cellRefByHeaderButton); 242 panel5.add(cellRefByHeaderLabel); 243 panel5.add(cellRefByHeader); 244 contentPane.add(panel5); 245 246 // add buttons at bottom of window 247 JPanel panel6 = new JPanel(); 248 panel6.setLayout(new FlowLayout()); 249 // Bottom Buttons - Cancel NamedTable 250 JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel")); // NOI18N 251 panel6.add(cancelButton); 252 cancelButton.addActionListener((e) -> { 253 finishDone(); 254 }); 255 // Bottom Buttons - Ok NamedTable 256 JButton okButton = new JButton(Bundle.getMessage("ButtonOK")); // NOI18N 257 panel6.add(okButton); 258 okButton.addActionListener((e) -> { 259 okPressed(e); 260 }); 261 // Delete NamedTable 262 JButton delete = new JButton(Bundle.getMessage("ButtonDelete")); // NOI18N 263 panel6.add(delete); 264 delete.addActionListener((e) -> { 265 deletePressed(); 266 }); 267 delete.setToolTipText(Bundle.getMessage("DeleteLogixNGButtonHint")); // NOI18N 268 contentPane.add(panel6); 269 } 270 271 _editLogixNGFrame.addWindowListener(new java.awt.event.WindowAdapter() { 272 @Override 273 public void windowClosing(java.awt.event.WindowEvent e) { 274 if (_inEditMode) { 275 okPressed(null); 276 } else { 277 finishDone(); 278 } 279 } 280 }); 281 _editLogixNGFrame.pack(); 282 _editLogixNGFrame.setVisible(true); 283 } 284 285 @Override 286 public void bringToFront() { 287 if (_editLogixNGFrame != null) { 288 _editLogixNGFrame.setVisible(true); 289 } 290 } 291 292 /** 293 * Display reminder to save. 294 */ 295 void showSaveReminder() { 296 if (_showReminder && !_checkEnabled) { 297 if (InstanceManager.getNullableDefault(jmri.UserPreferencesManager.class) != null) { 298 InstanceManager.getDefault(jmri.UserPreferencesManager.class). 299 showInfoMessage(Bundle.getMessage("ReminderTitle"), // NOI18N 300 Bundle.getMessage("ReminderSaveString", // NOI18N 301 Bundle.getMessage("MenuItemLogixNGTable")), // NOI18N 302 getClassName(), 303 "remindSaveLogixNG"); // NOI18N 304 } 305 } 306 } 307 308 /** 309 * Respond to the Ok button in the Edit NamedTable window. 310 * <p> 311 * Note: We also get here if the Edit NamedTable window is dismissed, or if the 312 * Add button is pressed in the LogixNG Table with an active Edit NamedTable 313 * window. 314 * 315 * @param e The event heard 316 */ 317 private void okPressed(ActionEvent e) { 318// if (checkEditConditionalNG()) { 319// return; 320// } 321 // Check if the User Name has been changed 322 String uName = editUserName.getText().trim(); 323 if (!(uName.equals(_curTable.getUserName()))) { 324 // user name has changed - check if already in use 325 if (uName.length() > 0) { 326 NamedTable p = _tableManager.getByUserName(uName); 327 if (p != null) { 328 // NamedTable with this user name already exists 329 log.error("Failure to update NamedTable with Duplicate User Name: {}", uName); // NOI18N 330 JmriJOptionPane.showMessageDialog(_editLogixNGFrame, 331 Bundle.getMessage("Error6"), 332 Bundle.getMessage("ErrorTitle"), // NOI18N 333 JmriJOptionPane.ERROR_MESSAGE); 334 return; 335 } 336 } 337 // user name is unique, change it 338 // user name is unique, change it 339 tableData.clear(); 340 tableData.put("chgUname", uName); // NOI18N 341 fireEditorEvent(); 342 } 343 if (_curTable instanceof DefaultCsvNamedTable) { 344 String csvFileName = editCsvTableName.getText().trim(); 345 346 try { 347 // NamedTable does not exist, create a new NamedTable 348 AbstractNamedTable.loadTableFromCSV_File( 349 "IQT1", // Arbitrary LogixNG table name 350// InstanceManager.getDefault(NamedTableManager.class).getAutoSystemName(), 351 null, csvFileName, false, ((DefaultCsvNamedTable) _curTable).getCsvType()); 352 } catch (java.nio.file.NoSuchFileException ex) { 353 log.error("Cannot load table due since the file is not found", ex); 354 JmriJOptionPane.showMessageDialog(_editLogixNGFrame, 355 Bundle.getMessage("TableEditor_Error_FileNotFound", csvFileName), 356 Bundle.getMessage("ErrorTitle"), // NOI18N 357 JmriJOptionPane.ERROR_MESSAGE); 358 return; 359 } catch (IOException ex) { 360 log.error("Cannot load table due to I/O error", ex); 361 JmriJOptionPane.showMessageDialog(_editLogixNGFrame, 362 ex.getLocalizedMessage(), 363 Bundle.getMessage("ErrorTitle"), // NOI18N 364 JmriJOptionPane.ERROR_MESSAGE); 365 return; 366 } catch (RuntimeException ex) { 367 log.error("Cannot load table due to an error", ex); 368 JmriJOptionPane.showMessageDialog(_editLogixNGFrame, 369 ex.getLocalizedMessage(), 370 Bundle.getMessage("ErrorTitle"), // NOI18N 371 JmriJOptionPane.ERROR_MESSAGE); 372 return; 373 } 374 375 ((DefaultCsvNamedTable)_curTable).setFileName(csvFileName); 376 } 377 // complete update and activate NamedTable 378 finishDone(); 379 } 380 381 void finishDone() { 382 showSaveReminder(); 383 _inEditMode = false; 384 if (_editLogixNGFrame != null) { 385 _editLogixNGFrame.setVisible(false); 386 _editLogixNGFrame.dispose(); 387 _editLogixNGFrame = null; 388 } 389 tableData.clear(); 390 tableData.put("Finish", _curTable.getSystemName()); // NOI18N 391 fireEditorEvent(); 392 } 393 394 /** 395 * Respond to the Delete button in the Edit NamedTable window. 396 */ 397 void deletePressed() { 398/* 399 if (!checkConditionalNGReferences(_curLogixNG.getSystemName())) { 400 return; 401 } 402*/ 403 _showReminder = true; 404 tableData.clear(); 405 tableData.put("Delete", _curTable.getSystemName()); // NOI18N 406 fireEditorEvent(); 407 finishDone(); 408 } 409 410 // ------------ Table Models ------------ 411 412 /** 413 * Table model for Tables in the Edit NamedTable pane. 414 */ 415 public final class TableTableModel extends AbstractTableModel { 416 417 @Override 418 public int getColumnCount() { 419 return _curTable.numColumns(); 420 } 421 422 @Override 423 public int getRowCount() { 424 return _curTable.numRows(); 425 } 426 427 @Override 428 public String getColumnName(int col) { 429 Object data = _curTable.getCell(0, col+1); 430 return data != null ? data.toString() : "<null>"; 431 } 432 433 @Override 434 public Object getValueAt(int row, int col) { 435 return _curTable.getCell(row+1, col+1); 436 } 437 } 438 439 private class RowHeaderListModel extends AbstractListModel<Object> { 440 @Override 441 public int getSize() { 442 return _curTable.numRows(); 443 } 444 445 @Override 446 public Object getElementAt(int index) { 447 // Ensure the header has at least five characters and ensure 448 // there are at least two spaces at the end since the last letter 449 // doesn't fully fit at the row. 450 Object data = _curTable.getCell(index+1, 0); 451 String padding = " "; // Two spaces 452 String str = data != null ? data.toString().concat(padding) : padding; 453 return str.length() < 5 ? str.concat(" ").substring(0, 7) : str; 454 } 455 } 456 457 private static final class RowHeaderRenderer extends JLabel implements ListCellRenderer<Object> { 458 459 RowHeaderRenderer(JTable table) { 460 JTableHeader header = table.getTableHeader(); 461 setOpaque(true); 462 setBorder(UIManager.getBorder("TableHeader.cellBorder")); 463 setHorizontalAlignment(CENTER); 464 setForeground(header.getForeground()); 465 setBackground(header.getBackground()); 466 setFont(header.getFont()); 467 } 468 469 @Override 470 public Component getListCellRendererComponent(JList<?> list, Object value, 471 int index, boolean isSelected, boolean cellHasFocus) { 472 setText((value == null) ? "" : value.toString()); 473 return this; 474 } 475 } 476 477 protected String getClassName() { 478 // The class that is returned must have a default constructor, 479 // a constructor with no parameters. 480 return jmri.jmrit.logixng.LogixNG_UserPreferences.class.getName(); 481 } 482 483 484 // ------------ NamedTable Notifications ------------ 485 // The Table views support some direct changes to the parent logix. 486 // This custom event is used to notify the parent NamedTable that changes are requested. 487 // When the event occurs, the parent NamedTable can retrieve the necessary information 488 // to carry out the actions. 489 // 490 // 1) Notify the calling NamedTable that the NamedTable user name has been changed. 491 // 2) Notify the calling NamedTable that the table view is closing 492 // 3) Notify the calling NamedTable that it is to be deleted 493 /** 494 * Create a custom listener event. 495 */ 496 public interface TableEventListener extends EventListener { 497 498 void tableEventOccurred(); 499 } 500 501 /** 502 * Maintain a list of listeners -- normally only one. 503 */ 504 List<EditorEventListener> listenerList = new ArrayList<>(); 505 506 /** 507 * This contains a list of commands to be processed by the listener 508 * recipient. 509 */ 510 private final HashMap<String, String> tableData = new HashMap<>(); 511 512 /** 513 * Add a listener. 514 * 515 * @param listener The recipient 516 */ 517 @Override 518 public void addEditorEventListener(EditorEventListener listener) { 519 listenerList.add(listener); 520 } 521 522 /** 523 * Remove a listener -- not used. 524 * 525 * @param listener The recipient 526 */ 527 @Override 528 public void removeEditorEventListener(EditorEventListener listener) { 529 listenerList.remove(listener); 530 } 531 532 /** 533 * Notify the listeners to check for new data. 534 */ 535 private void fireEditorEvent() { 536 for (EditorEventListener l : listenerList) { 537 l.editorEventOccurred(tableData); 538 } 539 } 540 541 542 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TableEditor.class); 543 544}