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}