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 if (csvType.equals(Table.CsvType.SEMICOLON)) {
138                    csvTypeLabel.setText(Table.CsvType.SEMICOLON.toString());
139                } else {
140                    throw new RuntimeException("unrecognized csvType");
141                }
142
143                csvTypePanel.add(csvTypeLabel);
144                contentPane.add(csvTypePanel);
145                JPanel panel4 = new JPanel();
146                panel4.setLayout(new FlowLayout());
147                JLabel tableFileNameLabel = new JLabel(Bundle.getMessage("TableEditor_FileName") + ": ");  // NOI18N
148                panel4.add(tableFileNameLabel);
149                editCsvTableName.setText(((DefaultCsvNamedTable)_curTable).getFileName());
150                editCsvTableName.setEditable(false);
151                panel4.add(editCsvTableName);
152                contentPane.add(panel4);
153            }
154
155
156            // add table of Tables
157            JPanel pctSpace = new JPanel();
158            pctSpace.setLayout(new FlowLayout());
159            pctSpace.add(new JLabel("   "));
160            contentPane.add(pctSpace);
161            JPanel pTitle = new JPanel();
162            pTitle.setLayout(new FlowLayout());
163            contentPane.add(pTitle);
164            // initialize table of conditionals
165            tableTableModel = new TableTableModel();
166            JTable tableTable = new JTable(tableTableModel);
167            tableTable.setCellSelectionEnabled(true);
168            tableTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
169            tableTable.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
170            tableTable.getTableHeader().setReorderingAllowed(false);
171
172            JButton cellRefByIndexButton = new JButton(Bundle.getMessage("TableEditor_CopyToClipboard"));  // NOI18N
173            JLabel cellRefByIndexLabel = new JLabel();  // NOI18N
174            JTextField cellRefByIndex = new JTextField();
175            cellRefByIndex.setEditable(false);
176            cellRefByIndexButton.setEnabled(false);
177
178            JButton cellRefByHeaderButton = new JButton(Bundle.getMessage("TableEditor_CopyToClipboard"));  // NOI18N
179            JLabel cellRefByHeaderLabel = new JLabel();  // NOI18N
180            JTextField cellRefByHeader = new JTextField();
181            cellRefByHeader.setEditable(false);
182            cellRefByHeaderButton.setEnabled(false);
183
184            java.awt.datatransfer.Clipboard clipboard =
185                    Toolkit.getDefaultToolkit().getSystemClipboard();
186
187            cellRefByIndexButton.addActionListener(
188                    (evt) -> { clipboard.setContents(new StringSelection(cellRefByIndexLabel.getText()), null);});
189
190            cellRefByHeaderButton.addActionListener(
191                    (evt) -> { clipboard.setContents(new StringSelection(cellRefByHeaderLabel.getText()), null);});
192
193            ListSelectionListener selectCellListener = (evt) -> {
194                String refByIndex = String.format("{%s[%d,%d]}", _curTable.getDisplayName(), tableTable.getSelectedRow()+1, tableTable.getSelectedColumn()+1);
195                cellRefByIndexLabel.setText(refByIndex);  // NOI18N
196                cellRefByIndex.setText(ReferenceUtil.getReference(symbolTable, refByIndex));  // NOI18N
197                cellRefByIndexButton.setEnabled(true);
198
199                Object rowHeaderObj = _curTable.getCell(tableTable.getSelectedRow()+1, 0);
200                Object columnHeaderObj = _curTable.getCell(0, tableTable.getSelectedColumn()+1);
201                String rowHeader = rowHeaderObj != null ? rowHeaderObj.toString() : "";
202                String columnHeader = columnHeaderObj != null ? columnHeaderObj.toString() : "";
203                if (!rowHeader.isEmpty() && !columnHeader.isEmpty()) {
204                    cellRefByHeaderButton.setEnabled(true);
205                    String refByHeader = String.format("{%s[%s,%s]}", _curTable.getDisplayName(), _curTable.getCell(tableTable.getSelectedRow()+1,0), _curTable.getCell(0,tableTable.getSelectedColumn()+1));
206                    cellRefByHeaderLabel.setText(refByHeader);  // NOI18N
207                    cellRefByHeader.setText(ReferenceUtil.getReference(symbolTable, refByIndex));  // NOI18N
208                } else {
209                    cellRefByHeaderButton.setEnabled(false);
210                    cellRefByHeaderLabel.setText("");    // NOI18N
211                    cellRefByHeader.setText("");        // NOI18N
212                }
213            };
214            tableTable.getSelectionModel().addListSelectionListener(selectCellListener);
215            tableTable.getColumnModel().getSelectionModel().addListSelectionListener(selectCellListener);
216
217            ListModel<Object> lm = new RowHeaderListModel();
218
219            JList<Object> rowHeader = new JList<>(lm);
220            rowHeader.setFixedCellHeight(
221                    tableTable.getRowHeight()
222//                    tableTable.getRowHeight() + tableTable.getRowMargin()
223//                    + table.getIntercellSpacing().height
224            );
225            rowHeader.setCellRenderer(new RowHeaderRenderer(tableTable));
226
227            JScrollPane tableTableScrollPane = new JScrollPane(tableTable);
228            tableTableScrollPane.setRowHeaderView(rowHeader);
229            Dimension dim = tableTable.getPreferredSize();
230            dim.height = 450;
231            tableTableScrollPane.getViewport().setPreferredSize(dim);
232            contentPane.add(tableTableScrollPane);
233
234            JPanel panel4 = new JPanel();
235            panel4.setLayout(new FlowLayout());
236            panel4.add(cellRefByIndexButton);
237            panel4.add(cellRefByIndexLabel);
238            panel4.add(cellRefByIndex);
239            contentPane.add(panel4);
240
241            JPanel panel5 = new JPanel();
242            panel5.setLayout(new FlowLayout());
243            panel5.add(cellRefByHeaderButton);
244            panel5.add(cellRefByHeaderLabel);
245            panel5.add(cellRefByHeader);
246            contentPane.add(panel5);
247
248            // add buttons at bottom of window
249            JPanel panel6 = new JPanel();
250            panel6.setLayout(new FlowLayout());
251            // Bottom Buttons - Cancel NamedTable
252            JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));  // NOI18N
253            panel6.add(cancelButton);
254            cancelButton.addActionListener((e) -> {
255                finishDone();
256            });
257            // Bottom Buttons - Ok NamedTable
258            JButton okButton = new JButton(Bundle.getMessage("ButtonOK"));  // NOI18N
259            panel6.add(okButton);
260            okButton.addActionListener((e) -> {
261                okPressed(e);
262            });
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        // The class that is returned must have a default constructor,
481        // a constructor with no parameters.
482        return jmri.jmrit.logixng.LogixNG_UserPreferences.class.getName();
483    }
484
485
486    // ------------ NamedTable Notifications ------------
487    // The Table views support some direct changes to the parent logix.
488    // This custom event is used to notify the parent NamedTable that changes are requested.
489    // When the event occurs, the parent NamedTable can retrieve the necessary information
490    // to carry out the actions.
491    //
492    // 1) Notify the calling NamedTable that the NamedTable user name has been changed.
493    // 2) Notify the calling NamedTable that the table view is closing
494    // 3) Notify the calling NamedTable that it is to be deleted
495    /**
496     * Create a custom listener event.
497     */
498    public interface TableEventListener extends EventListener {
499
500        void tableEventOccurred();
501    }
502
503    /**
504     * Maintain a list of listeners -- normally only one.
505     */
506    List<EditorEventListener> listenerList = new ArrayList<>();
507
508    /**
509     * This contains a list of commands to be processed by the listener
510     * recipient.
511     */
512    private final HashMap<String, String> tableData = new HashMap<>();
513
514    /**
515     * Add a listener.
516     *
517     * @param listener The recipient
518     */
519    @Override
520    public void addEditorEventListener(EditorEventListener listener) {
521        listenerList.add(listener);
522    }
523
524    /**
525     * Remove a listener -- not used.
526     *
527     * @param listener The recipient
528     */
529    @Override
530    public void removeEditorEventListener(EditorEventListener listener) {
531        listenerList.remove(listener);
532    }
533
534    /**
535     * Notify the listeners to check for new data.
536     */
537    private void fireEditorEvent() {
538        for (EditorEventListener l : listenerList) {
539            l.editorEventOccurred(tableData);
540        }
541    }
542
543
544    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TableEditor.class);
545
546}