001package jmri.jmrit.display;
002
003import java.awt.*;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import java.beans.PropertyChangeEvent;
007import java.beans.PropertyChangeListener;
008import java.util.*;
009import java.util.List;
010import java.util.regex.Matcher;
011import java.util.regex.Pattern;
012
013import javax.annotation.Nonnull;
014import javax.swing.*;
015import javax.swing.table.*;
016
017import jmri.*;
018import jmri.NamedBean.DisplayOptions;
019import jmri.jmrit.logixng.Module;
020import jmri.jmrit.logixng.*;
021import jmri.jmrit.logixng.NamedTable.NamedTablePropertyChangeEvent;
022import jmri.jmrit.logixng.actions.*;
023import jmri.jmrit.logixng.implementation.*;
024import jmri.jmrit.logixng.util.LogixNG_Thread;
025import jmri.util.swing.*;
026import jmri.util.table.ButtonEditor;
027import jmri.util.table.ButtonRenderer;
028
029/**
030 * An icon to display a NamedTable and let the user edit it.
031 *
032 * @author Pete Cressman    Copyright (c) 2009
033 * @author Daniel Bergqvist Copyright (C) 2025
034 * @since 5.15.1
035 */
036public final class LogixNGTableIcon extends PositionableJPanel {
037
038    private TableModel _tableModel = null;
039
040    // the associated NamedTable object
041    private final JTable _table;
042    private final JList<Object> _rowHeader;
043    private final JScrollPane _scrollPane;
044
045    private final java.awt.event.MouseListener _mouseListener = JmriMouseListener.adapt(this);
046
047    public LogixNGTableIcon(String tableName, Editor editor) {
048        super(editor);
049        setDisplayLevel(Editor.LABELS);
050
051        setLayout(new java.awt.BorderLayout());
052
053        _tableModel = new TableModel(tableName);
054
055        _table = new JTable(_tableModel);
056        _table.setCellSelectionEnabled(true);
057        _table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
058        _table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
059        _table.getTableHeader().setReorderingAllowed(false);
060
061        for (int col=0; col < _tableModel.getColumnCount(); col++) {
062            TableModel.HeaderType headerType = _tableModel._headers[col];
063            if (headerType._cellEditor != null) {
064                _table.getColumnModel().getColumn(col).setCellEditor(headerType._cellEditor);
065            }
066        }
067
068        ButtonRenderer buttonRenderer = new ButtonRenderer();
069        _table.setDefaultRenderer(JButton.class, buttonRenderer);
070        TableCellEditor editButEditor = new ButtonEditor(new JButton());
071        _table.setDefaultEditor(JButton.class, editButEditor);
072
073        ListModel<Object> lm = new RowHeaderListModel();
074
075        _rowHeader = new JList<>(lm);
076        _rowHeader.setFixedCellHeight(
077                _table.getRowHeight()
078        );
079        _rowHeader.setCellRenderer(new RowHeaderRenderer(_table));
080
081        _scrollPane = new JScrollPane(_table);
082        _scrollPane.setRowHeaderView(_rowHeader);
083        add(_scrollPane, BorderLayout.CENTER);
084
085        _table.addMouseListener(_mouseListener);
086        _rowHeader.addMouseListener(_mouseListener);
087        _scrollPane.addMouseListener(_mouseListener);
088        setPopupUtility(new PositionablePopupUtil(this, _table));
089    }
090
091    @Override
092    public Positionable deepClone() {
093        LogixNGTableIcon pos = new LogixNGTableIcon(
094                _tableModel.getTable().getDisplayName(), _editor);
095        return finishClone(pos);
096    }
097
098    protected Positionable finishClone(LogixNGTableIcon pos) {
099        _tableModel.setTable(_tableModel.getTable());
100        return super.finishClone(pos);
101    }
102
103    public TableModel getTableModel() {
104        return _tableModel;
105    }
106
107    public JTable getJTable() {
108        return _table;
109    }
110
111    @Override
112    @Nonnull
113    public String getTypeString() {
114        return Bundle.getMessage("PositionableType_LogixNGTableIcon");
115    }
116
117    @Override
118    public String getNameString() {
119        String name;
120        if (_tableModel.getTable() == null) {
121            name = Bundle.getMessage("NotConnected");
122        } else {
123            name = _tableModel.getTable().getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME);
124        }
125        return name;
126    }
127
128    private void configureLogixNGTable() {
129        NamedTable namedTable = _tableModel.getTable();
130
131        JComboBox<ModuleItem> _validateModuleComboBox;
132        _validateModuleComboBox = new JComboBox<>();
133        _validateModuleComboBox.addItem(new ModuleItem(null));
134        for (Module m : InstanceManager.getDefault(ModuleManager.class).getNamedBeanSet()) {
135            if ("DefaultFemaleDigitalActionSocket".equals(m.getRootSocketType().getName())
136                    && m.isVisible()) {
137                ModuleItem mi = new ModuleItem(m);
138                _validateModuleComboBox.addItem(mi);
139                Module _validateModule = _tableModel.getValidateModule();
140                if (_validateModule == m) {
141                    _validateModuleComboBox.setSelectedItem(mi);
142                }
143            }
144        }
145        JComboBoxUtil.setupComboBoxMaxRows(_validateModuleComboBox);
146
147        Map<String,Integer> columnIndexes = new HashMap<>();
148        List<String> columns = new ArrayList<>();
149        for (int col=0; col < namedTable.numColumns(); col++) {
150            String header = getObjectAsString(namedTable.getCell(0, col+1));
151            if (!header.isEmpty()) {
152                columns.add(header);
153                columnIndexes.put(header, col);
154            }
155        }
156        JList<String> columnList = new JList<>(columns.toArray(new String[0]));
157        for (String header : _tableModel._editableColumnsList) {
158            int index = columnIndexes.getOrDefault(header,-1);
159            if (index != -1) {
160                columnList.getSelectionModel().addSelectionInterval(index, index);
161            }
162        }
163        columnList.setBorder(BorderFactory.createLineBorder(Color.black));
164
165        JDialog dialog = new JDialog();
166        dialog.setTitle(Bundle.getMessage("ConfigureLogixNGTable"));
167        dialog.setLocationRelativeTo(this);
168        dialog.setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
169
170        JPanel p;
171        p = new JPanel();
172        p.setLayout(new java.awt.GridBagLayout());
173        java.awt.GridBagConstraints c = new java.awt.GridBagConstraints();
174        c.gridwidth = 1;
175        c.gridheight = 1;
176        c.gridx = 0;
177        c.gridy = 0;
178        c.anchor = java.awt.GridBagConstraints.EAST;
179        //c.gridx = 0;
180        c.gridy = 1;
181        JLabel validateLabel = new JLabel(Bundle.getMessage("LogixNGTableIcon_ValidateLogixNGModule"));
182        p.add(validateLabel, c);
183        validateLabel.setLabelFor(_validateModuleComboBox);
184        c.gridy = 2;
185        JLabel allowEditLabel = new JLabel(Bundle.getMessage("LogixNGTableIcon_AllowEditTable"));
186        JCheckBox allowEdit = new JCheckBox();
187        allowEdit.setSelected(_tableModel.isEditable());
188        p.add(allowEditLabel, c);
189        allowEditLabel.setLabelFor(allowEdit);
190        c.gridy = 3;
191        p.add(new JLabel(Bundle.getMessage("LogixNGTableIcon_EditableColumns")), c);
192        c.gridx = 1;
193        c.gridy = 0;
194        p.add(Box.createHorizontalStrut(5), c);
195        c.gridx = 2;
196        c.gridy = 2;
197        c.anchor = java.awt.GridBagConstraints.WEST;
198        c.weightx = 1.0;
199        c.fill = java.awt.GridBagConstraints.HORIZONTAL;  // text field will expand
200        p.add(allowEdit, c);
201        c.gridx = 2;
202        c.gridy = 1;
203        p.add(_validateModuleComboBox, c);
204//        sys.setToolTipText(Bundle.getMessage("SysNameToolTip", "Y"));
205        c.gridy = 3;
206        p.add(new JScrollPane(columnList), c);
207        c.gridy = 4;
208        p.add(Box.createVerticalStrut(5), c);
209        c.gridx = 0;
210        c.gridy = 5;
211        c.gridwidth = 3;
212        p.add(new JLabel(Bundle.getMessage("LogixNGTableIcon_EditableColumnsInfo")), c);
213        add(p);
214
215        // cancel + add buttons at bottom of window
216        JPanel panelBottom = new JPanel();
217        panelBottom.setLayout(new FlowLayout(FlowLayout.TRAILING));
218
219        JButton cancel;
220        panelBottom.add(cancel = new JButton(Bundle.getMessage("ButtonCancel")));
221        cancel.addActionListener((evt) -> {
222            dialog.dispose();
223        });
224
225        JButton ok;
226//        panelBottom.add(ok = new JButton(Bundle.getMessage(addButtonLabel)));
227        panelBottom.add(ok = new JButton(Bundle.getMessage("ButtonOK")));
228        ok.addActionListener((evt) -> {
229            _tableModel.setValidateModule(_validateModuleComboBox.getItemAt(_validateModuleComboBox.getSelectedIndex())._module);
230            _tableModel.setEditable(allowEdit.isSelected());
231            _tableModel.setEditableColumns(String.join("\t", columnList.getSelectedValuesList()));
232            dialog.dispose();
233        });
234//        p.add(panelBottom);
235
236        c.gridwidth = 3;
237        c.gridx = 0;
238        c.gridy = 99;
239        c.anchor = java.awt.GridBagConstraints.CENTER;
240        c.fill = java.awt.GridBagConstraints.NONE;
241        p.add(panelBottom, c);
242        add(p);
243
244        dialog.getContentPane().add(p);
245        dialog.pack();
246        dialog.setModal(true);
247        dialog.setVisible(true);
248    }
249
250    @Override
251    public boolean setEditIconMenu(javax.swing.JPopupMenu popup) {
252        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameLogixNGTable"));
253        popup.add(new javax.swing.AbstractAction(txt) {
254            @Override
255            public void actionPerformed(ActionEvent e) {
256                edit();
257            }
258        });
259
260        txt = Bundle.getMessage("ConfigureLogixNGTable");
261        popup.add(new javax.swing.AbstractAction(txt) {
262            @Override
263            public void actionPerformed(ActionEvent e) {
264                configureLogixNGTable();
265            }
266        });
267        return true;
268    }
269
270    @Override
271    protected void edit() {
272        makeIconEditorFrame(this, "LogixNGTable", true, _iconEditor);
273        _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.namedTablePickModelInstance());
274        ActionListener addIconAction = a -> editNamedTable();
275        _iconEditor.makeIconPanel(false);
276        _iconEditor.complete(addIconAction, false, false, true);
277        _iconEditor.setSelection(_tableModel.getTable());
278    }
279
280    void editNamedTable() {
281        _tableModel.setTable(_iconEditor.getTableSelection().getDisplayName());
282        _iconEditorFrame.dispose();
283        _iconEditorFrame = null;
284        _iconEditor = null;
285        validate();
286    }
287
288    @Override
289    void cleanup() {
290        if (_scrollPane != null) {
291            _table.removeMouseListener(_mouseListener);
292            _rowHeader.removeMouseListener(_mouseListener);
293            _scrollPane.removeMouseListener(_mouseListener);
294        }
295        _tableModel.setTable((NamedTable)null);
296        _tableModel.setValidateModule(null);
297    }
298
299    private static String getObjectAsString(Object o) {
300        return o != null ? o.toString() : "";
301    }
302
303    @Override
304    public void remove() {
305        _tableModel.dispose();
306    }
307
308    // ------------ Table Models ------------
309
310    /**
311     * Table model for Tables in the Edit NamedTable pane.
312     */
313    public final class TableModel extends AbstractTableModel
314            implements PropertyChangeListener {
315
316        private class HeaderType {
317            Class<?> _class;
318            String _type;
319            String _parameters;
320            String _header;
321            TableCellEditor _cellEditor;
322        }
323
324        private NamedBeanHandle<NamedTable> _namedTable;
325        private NamedBeanHandle<Module> _validateModule;
326        private boolean _editable = false;
327        private String _editableColumns = "";
328        List<String> _editableColumnsList;
329
330        private final LogixNG validateModuleLogixNG = new DefaultLogixNG("IQ:JMRI:LogixNGTableIcon", null);
331        private final ConditionalNG validateModuleConditionalNG =
332                new DefaultConditionalNG("IQC:JMRI:LogixNGTableIcon", null, LogixNG_Thread.DEFAULT_LOGIXNG_THREAD);
333        private final DigitalCallModule validateModuleAction = new DigitalCallModule("IQDA:JMRI:LogixNGTableIcon", null);
334        private Map<String, Object> _variablesWithValues;
335        private final MyData _myData = new MyData();
336        private final HeaderType[] _headers;
337
338
339        public TableModel(String tableName) {
340            initCallModule();
341            setEditableColumns("");
342            setTable(tableName);
343
344            if (_namedTable != null) {
345                _headers = new HeaderType[_namedTable.getBean().numColumns()];
346                initHeaders();
347            } else {
348                _headers = null;
349            }
350        }
351
352        private void initHeaders() {
353            for (int col=0; col < _namedTable.getBean().numColumns(); col++) {
354                HeaderType headerType = new HeaderType();
355                _headers[col] = headerType;
356
357                Object headerObj = _namedTable.getBean().getCell(0, col+1);
358                String header;
359                if (headerObj != null) {
360                    header = headerObj.toString();
361                } else {
362                    header = "";
363                }
364                if (header.startsWith("{{{")) {
365                    Pattern pattern = Pattern.compile("\\{\\{\\{(.+?)(\\:.+?)?\\}\\}\\}(.*)");
366                    Matcher matcher = pattern.matcher(header);
367                    if (matcher.matches()) {
368                        headerType._header = matcher.group(3);
369                        headerType._parameters = matcher.group(2);
370                        headerType._type = matcher.group(1).toLowerCase();
371                        switch (headerType._type) {
372                            case "button":
373                                headerType._class = JButton.class;
374                                break;
375                            case "list":
376                                String comboBoxTableName = headerType._parameters.substring(1);
377                                NamedTable comboBoxTable = InstanceManager
378                                        .getDefault(NamedTableManager.class)
379                                        .getNamedTable(comboBoxTableName);
380                                if (comboBoxTable != null) {
381                                    headerType._class = JComboBox.class;
382                                    JComboBox<String> comboBox = new JComboBox<>();
383                                    for (int row=1; row <= comboBoxTable.numRows(); row++) {
384                                        Object val = comboBoxTable.getCell(row, 1);
385                                        String value = val != null ? val.toString() : "";
386                                        comboBox.addItem(value);
387                                    }
388                                    headerType._cellEditor = new DefaultCellEditor(comboBox);
389                                } else {
390                                    log.warn("Cannot load LogixNG Table: {}", comboBoxTableName);
391                                }
392                                break;
393                            default:
394                                log.warn("Unknown column type: {}", matcher.group(1));
395                                headerType._type = null;
396                        }
397                    } else {
398                        log.warn("Unknown column definition: {}", header);
399                    }
400                } else {
401                    _headers[col]._header = header;
402                }
403            }
404        }
405
406        public NamedTable getTable() {
407            return _namedTable != null ? _namedTable.getBean() : null;
408        }
409
410        public void setTable(String tableName) {
411            if (_namedTable != null) {
412                _namedTable.getBean().removePropertyChangeListener(this);
413            }
414
415            NamedTable table = null;
416            if (tableName != null) {
417                table = InstanceManager.getDefault(NamedTableManager.class)
418                        .getNamedTable(tableName);
419            }
420
421            if (table != null) {
422                _namedTable = InstanceManager.getDefault(NamedBeanHandleManager.class)
423                        .getNamedBeanHandle(table.getDisplayName(), table);
424                if (_namedTable != null) {
425                    _namedTable.getBean().addPropertyChangeListener(this);
426                }
427            } else {
428                _namedTable = null;
429            }
430
431            fireTableStructureChanged();
432        }
433
434        public void setTable(NamedTable table) {
435            if (table != null) {
436                _namedTable = InstanceManager.getDefault(NamedBeanHandleManager.class)
437                        .getNamedBeanHandle(table.getDisplayName(), table);
438            } else {
439                _namedTable = null;
440            }
441            fireTableStructureChanged();
442        }
443
444        public Module getValidateModule() {
445            return _validateModule != null ? _validateModule.getBean() : null;
446        }
447
448        public void setValidateModule(Module module) {
449            if (module != null) {
450                _validateModule = InstanceManager.getDefault(NamedBeanHandleManager.class)
451                        .getNamedBeanHandle(module.getDisplayName(), module);
452            } else {
453                _validateModule = null;
454            }
455        }
456
457        public boolean isEditable() {
458            return _editable;
459        }
460
461        public void setEditable(boolean editable) {
462            _editable = editable;
463        }
464
465        public String getEditableColumns() {
466            return _editableColumns;
467        }
468
469        public void setEditableColumns(String editableColumns) {
470            _editableColumns = editableColumns;
471            _editableColumnsList = Arrays.asList(_editableColumns.split("\t"));
472        }
473
474        @Override
475        public int getColumnCount() {
476            if (_namedTable != null) {
477                return _namedTable.getBean().numColumns();
478            } else {
479                return 0;
480            }
481        }
482
483        @Override
484        public int getRowCount() {
485            if (_namedTable != null) {
486                return _namedTable.getBean().numRows();
487            } else {
488                return 0;
489            }
490        }
491
492        @Override
493        public String getColumnName(int col) {
494            return _headers[col]._header;
495        }
496
497        @Override
498        public Object getValueAt(int row, int col) {
499            if (_namedTable == null) {
500                return null;
501            }
502            return _namedTable.getBean().getCell(row+1, col+1);
503        }
504
505        @Override
506        public void setValueAt(Object val, int row, int col) {
507            callValidateModule(val, row, col);
508        }
509
510        @Override
511        public Class<?> getColumnClass(int col) {
512            Class<?> clazz = _headers[col]._class;
513            if (clazz != null) {
514                return clazz;
515            }
516            return super.getColumnClass(col);
517        }
518
519        @Override
520        public boolean isCellEditable(int rowIndex, int columnIndex) {
521            if (!_editable) {
522                return false;
523            }
524
525            String columnHeader = getObjectAsString(_namedTable.getBean().getCell(0, columnIndex+1));
526            boolean allowColumn = _editableColumns.isEmpty();
527            allowColumn |= _editableColumnsList.contains(columnHeader);
528
529            return allowColumn;
530        }
531
532        private void initCallModule() {
533            validateModuleLogixNG.addConditionalNG(validateModuleConditionalNG);
534
535            DigitalMany many = new DigitalMany("IQDA:JMRI:LogixNGTableIcon", null);
536            MaleSocket maleSocketMany = new DefaultMaleDigitalActionSocket(
537                    InstanceManager.getDefault(DigitalActionManager.class), many);
538            many.setParent(maleSocketMany);
539
540            maleSocketMany.addLocalVariable("iconId", SymbolTable.InitialValueType.String, null);
541            maleSocketMany.addLocalVariable("tableName", SymbolTable.InitialValueType.String, null);
542            maleSocketMany.addLocalVariable("row", SymbolTable.InitialValueType.String, null);
543            maleSocketMany.addLocalVariable("column", SymbolTable.InitialValueType.String, null);
544            maleSocketMany.addLocalVariable("type", SymbolTable.InitialValueType.String, null);
545            maleSocketMany.addLocalVariable("oldValue", SymbolTable.InitialValueType.String, null);
546            maleSocketMany.addLocalVariable("newValue", SymbolTable.InitialValueType.String, null);
547
548            try {
549                validateModuleConditionalNG.getFemaleSocket().connect(maleSocketMany);
550            } catch (SocketAlreadyConnectedException e) {
551                log.error("Exception when creating error handling LogixNG: ", e);
552            }
553
554            SetLocalVariables setLocalVariables = new SetLocalVariables("IQDA:JMRI:LogixNGTableIcon", null);
555            MaleSocket maleSocketSetLocalVariables = new DefaultMaleDigitalActionSocket(
556                    InstanceManager.getDefault(DigitalActionManager.class), setLocalVariables);
557            setLocalVariables.setParent(maleSocketSetLocalVariables);
558            _variablesWithValues = setLocalVariables.getMap();
559
560            try {
561                maleSocketMany.getChild(maleSocketMany.getChildCount()-1).connect(maleSocketSetLocalVariables);
562            } catch (SocketAlreadyConnectedException e) {
563                log.error("Exception when creating error handling LogixNG: ", e);
564            }
565
566            RunFinally runFinally = new RunFinally("IQDA:JMRI:LogixNGTableIcon", null, this::handleResult, _myData);
567            MaleSocket maleSocketRunFinally = new DefaultMaleDigitalActionSocket(
568                    InstanceManager.getDefault(DigitalActionManager.class), runFinally);
569            runFinally.setParent(maleSocketRunFinally);
570
571            try {
572                maleSocketMany.getChild(maleSocketMany.getChildCount()-1).connect(maleSocketRunFinally);
573            } catch (SocketAlreadyConnectedException e) {
574                log.error("Exception when creating error handling LogixNG: ", e);
575            }
576
577            validateModuleAction.addParameter("__iconId__", SymbolTable.InitialValueType.LocalVariable, "iconId", Module.ReturnValueType.None, null);
578            validateModuleAction.addParameter("__tableName__", SymbolTable.InitialValueType.LocalVariable, "tableName", Module.ReturnValueType.None, null);
579            validateModuleAction.addParameter("__row__", SymbolTable.InitialValueType.LocalVariable, "row", Module.ReturnValueType.None, null);
580            validateModuleAction.addParameter("__column__", SymbolTable.InitialValueType.LocalVariable, "column", Module.ReturnValueType.None, null);
581            validateModuleAction.addParameter("__type__", SymbolTable.InitialValueType.LocalVariable, "type", Module.ReturnValueType.None, null);
582            validateModuleAction.addParameter("__oldValue__", SymbolTable.InitialValueType.LocalVariable, "oldValue", Module.ReturnValueType.None, null);
583            validateModuleAction.addParameter("__newValue__", SymbolTable.InitialValueType.LocalVariable, "newValue", Module.ReturnValueType.LocalVariable, "newValue");
584            MaleSocket maleSocket = new DefaultMaleDigitalActionSocket(InstanceManager.getDefault(DigitalActionManager.class), validateModuleAction);
585            validateModuleAction.setParent(maleSocket);
586            try {
587                runFinally.getChild(0).connect(maleSocket);
588            } catch (SocketAlreadyConnectedException e) {
589                log.error("Exception when creating error handling LogixNG: ", e);
590            }
591            List<String> errors = new ArrayList<>();
592            validateModuleLogixNG.setParentForAllChildren(errors);
593            if (!errors.isEmpty()) {
594                for (String s : errors) {
595                    log.error("Error: {}", s);
596                }
597            }
598        }
599
600        private void callValidateModule(Object val, int row, int col) {
601            if (_validateModule == null ||
602                    !_validateModule.getBean().getRootSocket().isConnected()) {
603                _namedTable.getBean().setCell(val, row+1, col+1);
604                return;
605            }
606            validateModuleAction.getSelectNamedBean().setNamedBean(_validateModule);
607
608            NamedTable table = _namedTable.getBean();
609            _variablesWithValues.put("iconId", LogixNGTableIcon.this.getId());
610            _variablesWithValues.put("tableName", _namedTable.getName());
611            _variablesWithValues.put("row", table.getCell(row+1, 0));
612            _variablesWithValues.put("column", table.getCell(0, col+1));
613            _variablesWithValues.put("type", _headers[col]._type);
614            _variablesWithValues.put("oldValue", table.getCell(row+1, col+1));
615            _variablesWithValues.put("newValue", val);
616            _myData.setValues(_namedTable.getBean(), row, col);
617            validateModuleConditionalNG.execute();
618        }
619
620        private void handleResult(ConditionalNG conditionalNG, Exception ex, RunFinally.Data data) {
621            if (ex != null) {
622                if (ex instanceof ValidationErrorException) {
623                    JOptionPane.showMessageDialog(LogixNGTableIcon.this,
624                            ex.getMessage(),
625                            Bundle.getMessage("LogixNGTableIcon_ValidationError"),
626                            JOptionPane.ERROR_MESSAGE);
627                } else {
628                    JOptionPane.showMessageDialog(LogixNGTableIcon.this,
629                            ex.getMessage(),
630                            Bundle.getMessage("LogixNGTableIcon_Error"),
631                            JOptionPane.ERROR_MESSAGE);
632                }
633            } else {
634                if (!(data instanceof MyData)) {
635                    throw new IllegalArgumentException("data is not a MyData");
636                }
637                MyData myData = (MyData)data;
638                Object newValue = conditionalNG.getSymbolTable().getValue("newValue");
639                myData._namedTable.setCell(newValue, myData._row+1, myData._col+1);
640                TableModel.this.fireTableCellUpdated(myData._row, myData._col);
641            }
642        }
643
644        @Override
645        public void propertyChange(PropertyChangeEvent evt) {
646            if (evt instanceof NamedTablePropertyChangeEvent) {
647                var tableEvt = (NamedTablePropertyChangeEvent)evt;
648                if (NamedTable.PROPERTY_CELL_CHANGED.equals(evt.getPropertyName())) {
649                    fireTableCellUpdated(tableEvt.getRow()-1, tableEvt.getColumn()-1);
650                }
651            }
652        }
653
654        private void dispose() {
655            if (_namedTable != null) {
656                _namedTable.getBean().removePropertyChangeListener(this);
657            }
658        }
659
660    }
661
662
663    private static class MyData implements RunFinally.Data {
664
665        private NamedTable _namedTable;
666        private int _row;
667        private int _col;
668
669        private void setValues(NamedTable namedTable, int row, int col) {
670            this._namedTable = namedTable;
671            this._row = row;
672            this._col = col;
673        }
674    }
675
676
677    private class RowHeaderListModel extends AbstractListModel<Object> {
678        @Override
679        public int getSize() {
680            return _tableModel.getTable().numRows();
681        }
682
683        @Override
684        public Object getElementAt(int index) {
685            // Ensure the header has at least five characters and ensure
686            // there are at least two spaces at the end since the last letter
687            // doesn't fully fit at the row.
688            Object data = _tableModel.getTable().getCell(index+1, 0);
689            String padding = "  ";     // Two spaces
690            String str = data != null ? data.toString().concat(padding) : padding;
691            return str.length() < 5 ? str.concat("     ").substring(0, 7) : str;
692        }
693
694    }
695
696
697    private static final class RowHeaderRenderer extends JLabel implements ListCellRenderer<Object> {
698
699        RowHeaderRenderer(JTable table) {
700            JTableHeader header = table.getTableHeader();
701            setOpaque(true);
702            setBorder(UIManager.getBorder("TableHeader.cellBorder"));
703            setHorizontalAlignment(CENTER);
704            setForeground(header.getForeground());
705            setBackground(header.getBackground());
706            setFont(header.getFont());
707        }
708
709        @Override
710        public Component getListCellRendererComponent(JList<?> list, Object value,
711                int index, boolean isSelected, boolean cellHasFocus) {
712            setText((value == null) ? "" : value.toString());
713            return this;
714        }
715    }
716
717
718    private static class ModuleItem {
719
720        private final Module _module;
721
722        public ModuleItem(Module m) {
723            _module = m;
724        }
725
726        @Override
727        public String toString() {
728            if (_module == null) return "";
729            else return _module.getDisplayName();
730        }
731    }
732
733
734    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogixNGTableIcon.class);
735}