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// done.setToolTipText(Bundle.getMessage("CancelButtonHint")); // NOI18N 256 // Bottom Buttons - Ok NamedTable 257 JButton okButton = new JButton(Bundle.getMessage("ButtonOK")); // NOI18N 258 panel6.add(okButton); 259 okButton.addActionListener((e) -> { 260 okPressed(e); 261 }); 262// done.setToolTipText(Bundle.getMessage("OkButtonHint")); // NOI18N 263 // Delete NamedTable 264 JButton delete = new JButton(Bundle.getMessage("ButtonDelete")); // NOI18N 265 panel6.add(delete); 266 delete.addActionListener((e) -> { 267 deletePressed(); 268 }); 269 delete.setToolTipText(Bundle.getMessage("DeleteLogixNGButtonHint")); // NOI18N 270 contentPane.add(panel6); 271 } 272 273 _editLogixNGFrame.addWindowListener(new java.awt.event.WindowAdapter() { 274 @Override 275 public void windowClosing(java.awt.event.WindowEvent e) { 276 if (_inEditMode) { 277 okPressed(null); 278 } else { 279 finishDone(); 280 } 281 } 282 }); 283 _editLogixNGFrame.pack(); 284 _editLogixNGFrame.setVisible(true); 285 } 286 287 @Override 288 public void bringToFront() { 289 if (_editLogixNGFrame != null) { 290 _editLogixNGFrame.setVisible(true); 291 } 292 } 293 294 /** 295 * Display reminder to save. 296 */ 297 void showSaveReminder() { 298 if (_showReminder && !_checkEnabled) { 299 if (InstanceManager.getNullableDefault(jmri.UserPreferencesManager.class) != null) { 300 InstanceManager.getDefault(jmri.UserPreferencesManager.class). 301 showInfoMessage(Bundle.getMessage("ReminderTitle"), // NOI18N 302 Bundle.getMessage("ReminderSaveString", // NOI18N 303 Bundle.getMessage("MenuItemLogixNGTable")), // NOI18N 304 getClassName(), 305 "remindSaveLogixNG"); // NOI18N 306 } 307 } 308 } 309 310 /** 311 * Respond to the Ok button in the Edit NamedTable window. 312 * <p> 313 * Note: We also get here if the Edit NamedTable window is dismissed, or if the 314 * Add button is pressed in the LogixNG Table with an active Edit NamedTable 315 * window. 316 * 317 * @param e The event heard 318 */ 319 private void okPressed(ActionEvent e) { 320// if (checkEditConditionalNG()) { 321// return; 322// } 323 // Check if the User Name has been changed 324 String uName = editUserName.getText().trim(); 325 if (!(uName.equals(_curTable.getUserName()))) { 326 // user name has changed - check if already in use 327 if (uName.length() > 0) { 328 NamedTable p = _tableManager.getByUserName(uName); 329 if (p != null) { 330 // NamedTable with this user name already exists 331 log.error("Failure to update NamedTable with Duplicate User Name: {}", uName); // NOI18N 332 JmriJOptionPane.showMessageDialog(_editLogixNGFrame, 333 Bundle.getMessage("Error6"), 334 Bundle.getMessage("ErrorTitle"), // NOI18N 335 JmriJOptionPane.ERROR_MESSAGE); 336 return; 337 } 338 } 339 // user name is unique, change it 340 // user name is unique, change it 341 tableData.clear(); 342 tableData.put("chgUname", uName); // NOI18N 343 fireEditorEvent(); 344 } 345 if (_curTable instanceof DefaultCsvNamedTable) { 346 String csvFileName = editCsvTableName.getText().trim(); 347 348 try { 349 // NamedTable does not exist, create a new NamedTable 350 AbstractNamedTable.loadTableFromCSV_File( 351 "IQT1", // Arbitrary LogixNG table name 352// InstanceManager.getDefault(NamedTableManager.class).getAutoSystemName(), 353 null, csvFileName, false, ((DefaultCsvNamedTable) _curTable).getCsvType()); 354 } catch (java.nio.file.NoSuchFileException ex) { 355 log.error("Cannot load table due since the file is not found", ex); 356 JmriJOptionPane.showMessageDialog(_editLogixNGFrame, 357 Bundle.getMessage("TableEditor_Error_FileNotFound", csvFileName), 358 Bundle.getMessage("ErrorTitle"), // NOI18N 359 JmriJOptionPane.ERROR_MESSAGE); 360 return; 361 } catch (IOException ex) { 362 log.error("Cannot load table due to I/O error", ex); 363 JmriJOptionPane.showMessageDialog(_editLogixNGFrame, 364 ex.getLocalizedMessage(), 365 Bundle.getMessage("ErrorTitle"), // NOI18N 366 JmriJOptionPane.ERROR_MESSAGE); 367 return; 368 } catch (RuntimeException ex) { 369 log.error("Cannot load table due to an error", ex); 370 JmriJOptionPane.showMessageDialog(_editLogixNGFrame, 371 ex.getLocalizedMessage(), 372 Bundle.getMessage("ErrorTitle"), // NOI18N 373 JmriJOptionPane.ERROR_MESSAGE); 374 return; 375 } 376 377 ((DefaultCsvNamedTable)_curTable).setFileName(csvFileName); 378 } 379 // complete update and activate NamedTable 380 finishDone(); 381 } 382 383 void finishDone() { 384 showSaveReminder(); 385 _inEditMode = false; 386 if (_editLogixNGFrame != null) { 387 _editLogixNGFrame.setVisible(false); 388 _editLogixNGFrame.dispose(); 389 _editLogixNGFrame = null; 390 } 391 tableData.clear(); 392 tableData.put("Finish", _curTable.getSystemName()); // NOI18N 393 fireEditorEvent(); 394 } 395 396 /** 397 * Respond to the Delete button in the Edit NamedTable window. 398 */ 399 void deletePressed() { 400/* 401 if (!checkConditionalNGReferences(_curLogixNG.getSystemName())) { 402 return; 403 } 404*/ 405 _showReminder = true; 406 tableData.clear(); 407 tableData.put("Delete", _curTable.getSystemName()); // NOI18N 408 fireEditorEvent(); 409 finishDone(); 410 } 411 412 // ------------ Table Models ------------ 413 414 /** 415 * Table model for Tables in the Edit NamedTable pane. 416 */ 417 public final class TableTableModel extends AbstractTableModel { 418 419 @Override 420 public int getColumnCount() { 421 return _curTable.numColumns(); 422 } 423 424 @Override 425 public int getRowCount() { 426 return _curTable.numRows(); 427 } 428 429 @Override 430 public String getColumnName(int col) { 431 Object data = _curTable.getCell(0, col+1); 432 return data != null ? data.toString() : "<null>"; 433 } 434 435 @Override 436 public Object getValueAt(int row, int col) { 437 return _curTable.getCell(row+1, col+1); 438 } 439 } 440 441 private class RowHeaderListModel extends AbstractListModel<Object> { 442 @Override 443 public int getSize() { 444 return _curTable.numRows(); 445 } 446 447 @Override 448 public Object getElementAt(int index) { 449 // Ensure the header has at least five characters and ensure 450 // there are at least two spaces at the end since the last letter 451 // doesn't fully fit at the row. 452 Object data = _curTable.getCell(index+1, 0); 453 String padding = " "; // Two spaces 454 String str = data != null ? data.toString().concat(padding) : padding; 455 return str.length() < 5 ? str.concat(" ").substring(0, 7) : str; 456 } 457 } 458 459 private static final class RowHeaderRenderer extends JLabel implements ListCellRenderer<Object> { 460 461 RowHeaderRenderer(JTable table) { 462 JTableHeader header = table.getTableHeader(); 463 setOpaque(true); 464 setBorder(UIManager.getBorder("TableHeader.cellBorder")); 465 setHorizontalAlignment(CENTER); 466 setForeground(header.getForeground()); 467 setBackground(header.getBackground()); 468 setFont(header.getFont()); 469 } 470 471 @Override 472 public Component getListCellRendererComponent(JList<?> list, Object value, 473 int index, boolean isSelected, boolean cellHasFocus) { 474 setText((value == null) ? "" : value.toString()); 475 return this; 476 } 477 } 478 479 protected String getClassName() { 480 return TableEditor.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}