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}