001package jmri.jmrix.openlcb.swing.lccpro;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005
006import javax.swing.Icon;
007import javax.swing.ImageIcon;
008import javax.swing.JButton;
009import javax.swing.JLabel;
010import javax.swing.table.DefaultTableModel;
011
012import jmri.jmrix.can.CanSystemConnectionMemo;
013
014import org.openlcb.*;
015
016/**
017 * Table data model for display of LCC node values.
018 * <p>
019 * Any desired ordering, etc, is handled outside this class.
020 *
021 * @author Bob Jacobsen Copyright (C) 2009, 2010, 2024
022 * @since 5.11.1
023 */
024public class LccProTableModel extends DefaultTableModel implements PropertyChangeListener {
025
026    static final int NAMECOL = 0;
027    public static final int IDCOL = 1;
028    static final int MFGCOL = 2;
029    static final int MODELCOL = 3;
030    static final int SVERSIONCOL = 4;
031    public static final int CONFIGURECOL = 5;
032    public static final int UPGRADECOL = 6;
033    public static final int DESCRIPTIONCOL = 7;
034    public static final int NUMCOL = DESCRIPTIONCOL + 1;
035
036    CanSystemConnectionMemo memo;
037    MimicNodeStore nodestore;
038    
039    public LccProTableModel(CanSystemConnectionMemo memo) {
040        this.memo = memo;
041        this.nodestore = memo.get(MimicNodeStore.class);
042        log.trace("Found nodestore {}", nodestore);
043        nodestore.addPropertyChangeListener(this);
044        nodestore.refresh();
045    }
046
047
048    @Override
049    public void propertyChange(PropertyChangeEvent e) {
050        // set this up to fireTableDataChanged() when a new node appears
051        log.trace("received {}", e);
052        // SNIP might take a bit, so fire slightly later
053        jmri.util.ThreadingUtil.runOnGUIDelayed( ()->{ 
054            fireTableDataChanged(); 
055        }, 250);
056        jmri.util.ThreadingUtil.runOnGUIDelayed( ()->{ 
057            fireTableDataChanged(); 
058        }, 1000);
059    }
060
061    @Override
062    public int getRowCount() {
063        if (nodestore == null) {
064            log.trace("Did not expect null nodestore, except in initialization before ctor runs");
065            return 1;
066        }
067        if (nodestore.getNodeMemos() == null) {
068            log.debug("Did not expect missing node memos");
069            return 0;
070        }
071        return nodestore.getNodeMemos().size();
072    }
073
074    @Override
075    public int getColumnCount() {
076        return NUMCOL;
077    }
078
079    @Override
080    public String getColumnName(int col) {
081        switch (col) {
082            case NAMECOL:
083                return Bundle.getMessage("FieldName");
084            case IDCOL:
085                return Bundle.getMessage("FieldID");
086            case MFGCOL:
087                return Bundle.getMessage("FieldMfg");
088            case MODELCOL:
089                return Bundle.getMessage("FieldModel");
090            case SVERSIONCOL:
091                return Bundle.getMessage("FieldSVersion");
092            case CONFIGURECOL:
093                return Bundle.getMessage("FieldConfig");
094            case UPGRADECOL:
095                return Bundle.getMessage("FieldUpgrade");
096            case DESCRIPTIONCOL:
097                return Bundle.getMessage("FieldDescription");
098            default:
099                return "<unexpected column number>";
100        }
101    }
102
103    @Override
104    public Class<?> getColumnClass(int col) {
105        switch (col) {
106            case CONFIGURECOL:
107            case UPGRADECOL:
108                return JButton.class;
109            default:
110                return String.class;
111        }
112    }
113
114    /**
115     * {@inheritDoc}
116     * <p>
117     * Note that the table can be set to be non-editable when constructed, in
118     * which case this always returns false.
119     *
120     * @return true if cell is editable 
121     */
122    @Override
123    public boolean isCellEditable(int row, int col) {
124        switch (col) {
125            case CONFIGURECOL:
126            case UPGRADECOL:
127                return true;
128            default:
129                return false;
130        }
131    }
132
133    /**
134     * {@inheritDoc}
135     *
136     * Provides an empty string for a column if the model returns null for that
137     * value.
138     */
139    @Override
140    public Object getValueAt(int row, int col) {
141        log.trace("getValue({}, {})", row, col);
142        
143        var memoArray = nodestore.getNodeMemos().toArray(new MimicNodeStore.NodeMemo[0]);
144        if (row >= memoArray.length) return "";  // sometimes happens at startup
145        var nodememo = memoArray[row];
146        if (nodememo == null) return "<invalid node memo>";
147        var snip = nodememo.getSimpleNodeIdent();
148        if (snip == null) return "<snip info not yet availble>";
149        var pip = nodememo.getProtocolIdentification();
150
151        switch (col) {
152            case NAMECOL:
153                return snip.getUserName();
154            case IDCOL:
155                return nodememo.getNodeID().toString();
156            case MFGCOL:
157                return snip.getMfgName();
158            case MODELCOL:
159                return snip.getModelName();
160            case SVERSIONCOL:
161                return snip.getSoftwareVersion();
162            case CONFIGURECOL:
163                if (pip.hasProtocol(ProtocolIdentification.Protocol.ConfigurationDescription)) {
164                    return Bundle.getMessage("FieldConfig");
165                } else {
166                    return null;
167                }
168            case UPGRADECOL:
169                if (pip.hasProtocol(ProtocolIdentification.Protocol.FirmwareUpgrade)) {
170                    return Bundle.getMessage("FieldUpgrade");
171                } else {
172                    return null;
173                }
174            case DESCRIPTIONCOL:
175                return snip.getUserDesc();
176            default:
177                return "<unexpected column number>";
178        }
179    }
180
181    @Override
182    public void setValueAt(Object value, int row, int col) {
183        log.trace("getValue({}, {})", row, col);
184        MimicNodeStore.NodeMemo nodememo = nodestore.getNodeMemos().toArray(new MimicNodeStore.NodeMemo[0])[row];
185        if (nodememo == null) {
186            log.error("Button pushed but no corresponding node for row {}", row);
187            return;
188        }
189        var pip = nodememo.getProtocolIdentification();
190
191        switch (col) {
192            case CONFIGURECOL:
193                if (pip.hasProtocol(ProtocolIdentification.Protocol.ConfigurationDescription)) {
194                    var actions = new jmri.jmrix.openlcb.swing.ClientActions(memo.get(org.openlcb.OlcbInterface.class), memo);
195                    var node = nodememo.getNodeID();
196                    var description = jmri.jmrix.openlcb.swing.networktree.NetworkTreePane.augmentedNodeName(nodememo);
197                    actions.openCdiWindow(node, description);
198                    // We want the table to retain focus while the CDI loads
199                    // This also removes the selection from this cell, so that cmd-`
200                    // no longer repeats the action of pressing the button
201                    forceFocus();
202                }
203                break;
204            case UPGRADECOL:
205                if (pip.hasProtocol(ProtocolIdentification.Protocol.FirmwareUpgrade)) {
206                    var node = nodememo.getNodeID();
207                    var action = new jmri.jmrix.openlcb.swing.downloader.LoaderPane.Default(node);
208                    action.actionPerformed(null);
209                }
210                break;
211            default:
212                break;
213        }
214    }
215
216    // to be overridden at construction time with e.g.
217    //      frame.toFront();
218    //      frame.requestFocus();
219    //
220    public void forceFocus() {
221    }
222    
223    public int getPreferredWidth(int column) {
224        int retval = 20; // always take some width
225        retval = Math.max(retval, new JLabel(getColumnName(column))
226            .getPreferredSize().width + 15);  // leave room for sorter arrow
227        for (int row = 0; row < getRowCount(); row++) {
228            if (getColumnClass(column).equals(String.class)) {
229                retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width);
230            } else if (getColumnClass(column).equals(Integer.class)) {
231                retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width);
232            } else if (getColumnClass(column).equals(ImageIcon.class)) {
233                retval = Math.max(retval, new JLabel((Icon) getValueAt(row, column)).getPreferredSize().width);
234            }
235        }
236        return retval + 5;
237    }
238
239    // drop listeners
240    public void dispose() {
241        nodestore.removePropertyChangeListener(this);
242    }
243
244    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LccProTableModel.class);
245}