001package jmri.jmrit.symbolicprog;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.beans.PropertyChangeEvent;
006import java.beans.PropertyChangeListener;
007import java.util.ArrayList;
008import java.util.List;
009import javax.swing.JButton;
010import javax.swing.JLabel;
011import javax.swing.table.AbstractTableModel;
012import jmri.Programmer;
013import jmri.ProgrammingMode;
014import jmri.util.jdom.LocaleSelector;
015import org.jdom2.Element;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * Holds a table of the extra menu items available for a particular decoder.
021 *
022 * @see ResetTableModel
023 *
024 * @author Howard G. Penny Copyright (C) 2005
025 * @author Bob Jacobsen   Copyright (C) 2021
026 */
027public class ExtraMenuTableModel extends AbstractTableModel implements ActionListener, PropertyChangeListener {
028
029    private String headers[] = {"Label", "Name",
030        "Value",
031        "Write", "State"};
032
033    private List<CvValue> rowVector = new ArrayList<>(); // vector of Reset items
034    private List<String> labelVector = new ArrayList<>(); // vector of related labels
035    private List<List<String>> modeVector = new ArrayList<>(); // vector of related modes
036
037    private List<JButton> _writeButtons = new ArrayList<>();
038
039    private JLabel _status = null;
040    private Programmer mProgrammer;
041
042    String name = "<default>"; // User visible menu name
043
044    public String getName() { return name; }
045    public void setName(String n) { this.name = n; }
046
047    public ExtraMenuTableModel(JLabel status, Programmer pProgrammer) {
048        super();
049
050        mProgrammer = pProgrammer;
051        // save a place for notification
052        _status = status;
053    }
054
055    @Override
056    public String toString() {
057        return "Element id: "+getTopLevelElementName()+" name: "+name+": "+rowVector.size()+" rows";
058    }
059
060    public void setProgrammer(Programmer p) {
061        mProgrammer = p;
062
063        // pass on to all contained CVs
064        rowVector.forEach((cv) -> {
065            cv.setProgrammer(p);
066        });
067    }
068
069    private boolean hasOpsModeFlag = false;
070
071    protected void flagIfOpsMode(String mode) {
072        log.trace(" flagIfOpsMode {}", mode);
073        if (mode.contains("OPS")) {
074            hasOpsModeFlag = true;
075        }
076    }
077
078    public boolean hasOpsModeReset() {
079        return hasOpsModeFlag;
080    }
081
082    @Override
083    public int getRowCount() {
084        return rowVector.size();
085    }
086
087    @Override
088    public int getColumnCount() {
089        return headers.length;
090    }
091
092    @Override
093    public Object getValueAt(int row, int col) {
094        // log.debug("getValueAt "+row+" "+col);
095        // some error checking
096        if (row >= rowVector.size()) {
097            log.debug("row greater than row vector");
098            return "Error";
099        }
100        CvValue cv = rowVector.get(row);
101        if (cv == null) {
102            log.debug("cv is null!");
103            return "Error CV";
104        }
105        switch (headers[col]) {
106            case "Label":
107                return "" + labelVector.get(row);
108            case "Name":
109                return "" + cv.cvName();
110            case "Value":
111                return "" + cv.getValue();
112            case "Write":
113                return _writeButtons.get(row);
114            case "State":
115                AbstractValue.ValueState state = cv.getState();
116                switch (state) {
117                    case UNKNOWN:
118                        return "Unknown";
119                    case READ:
120                        return "Read";
121                    case EDITED:
122                        return "Edited";
123                    case STORED:
124                        return "Stored";
125                    case FROMFILE:
126                        return "From file";
127                    default:
128                        return "inconsistent";
129                }
130            default:
131                return "hmmm ... missed it";
132        }
133    }
134
135    public void setRow(int row, Element e, Element p, String model) {
136        decoderModel = model; // Save for use elsewhere
137        String label = LocaleSelector.getAttribute(e, "label"); // Note the name variable is actually the label attribute
138        log.debug("Starting to setRow \"{}\"", label);
139        String cv = e.getAttribute("CV").getValue();
140        int cvVal = Integer.parseInt(e.getAttribute("default").getValue());
141
142        log.debug("            CV \"{}\" value {}", cv, cvVal);
143
144        CvValue resetCV = new CvValue(cv, mProgrammer);
145        resetCV.addPropertyChangeListener(this);
146        resetCV.setValue(cvVal);
147        resetCV.setWriteOnly(true);
148        resetCV.setState(AbstractValue.ValueState.STORED);
149        rowVector.add(resetCV);
150        labelVector.add(label);
151        modeVector.add(getResetModeList(e, p));
152    }
153
154    protected List<String> getResetModeList(Element e, Element p) {
155        List<Element> elementList = new ArrayList<>();
156        List<String> modeList = new ArrayList<>();
157        List<Element> elementModes;
158        String mode;
159        boolean resetsModeFound = false;
160
161        elementList.add(p);
162        elementList.add(e);
163
164        for (Element ep : elementList) {
165            try {
166                mode = ep.getAttribute("mode").getValue();
167                if (ep.getName().equals(getTopLevelElementName())) {
168                    resetsModeFound = true;
169                } else if (resetsModeFound) {
170                    modeList.clear();
171                    resetsModeFound = false;
172                }
173                modeList.add(mode);
174                flagIfOpsMode(mode);
175            } catch (NullPointerException ex) {
176                // ignore as expected result if there is no attribute mode
177            }
178
179            try {
180                elementModes = ep.getChildren("mode");
181                for (Element s : elementModes) {
182                    if (ep.getName().equals(getTopLevelElementName())) {
183                        resetsModeFound = true;
184                    } else if (resetsModeFound) {
185                        modeList.clear();
186                        resetsModeFound = false;
187                    }
188                    modeList.add(s.getText());
189                    flagIfOpsMode(s.getText());
190                }
191            } catch (NullPointerException ex) {
192                // ignore as expected result if there is no attribute mode
193            }
194        }
195
196        return modeList;
197    }
198
199    /**
200     * Name of the XML element for the collection of extra menu items
201     * @return element name for top level menu item
202     */
203    public String getTopLevelElementName() {
204        return "resets";
205    }
206
207    /**
208     * Name of the XML element for individual menu items
209     * @return element name for individual menu item
210     */
211    public String getIndividualElementName() {
212        return "factReset";
213    }
214
215    private ProgrammingMode savedMode;
216    private String decoderModel;
217
218    protected void performReset(int row) {
219        savedMode = mProgrammer.getMode(); // In case we need to change modes
220        if (modeVector.get(row) != null) {
221            List<ProgrammingMode> modes = mProgrammer.getSupportedModes();
222            List<String> validModes = modeVector.get(row);
223
224            StringBuilder programmerModeListBuffer = new StringBuilder("");
225            modes.forEach((m) -> {
226                programmerModeListBuffer.append(",").append(m.toString());
227            });
228            String programmerModeList = programmerModeListBuffer.toString();
229            if (programmerModeList.length() <= 1) {
230                programmerModeList = ""; // NOI18N
231            } else if (programmerModeList.startsWith(",")) {
232                programmerModeList = programmerModeList.substring(1);
233            }
234
235            StringBuilder resetModeBuilder = new StringBuilder("");
236            validModes.forEach((mode) -> {
237                resetModeBuilder.append(",").append(new ProgrammingMode(mode).toString());
238            });
239            String resetModeList = resetModeBuilder.toString();
240            if (resetModeList.length() <= 1) {
241                resetModeList = ""; // NOI18N
242            } else if (resetModeList.startsWith(",")) {
243                resetModeList = resetModeList.substring(1);
244            }
245
246            if (resetModeList.length() > 0) {
247                boolean modeFound = false;
248                search:
249                for (ProgrammingMode m : modes) {
250                    for (String mode : validModes) {
251                        if (mode.equals(m.getStandardName())) {
252                            mProgrammer.setMode(m);
253                            modeFound = true;
254                            break search;
255                        }
256                    }
257                }
258
259                if (mProgrammer.getMode().getStandardName().contains("OPS")) {
260                    if (!opsResetOk()) {
261                        return;
262                    }
263                }
264
265                if (!modeFound) {
266                    if (!badModeOk((savedMode.toString()), resetModeList, programmerModeList)) {
267                        return;
268                    }
269                    log.warn("{} for {} was attempted in {} mode.", labelVector.get(row), decoderModel, savedMode);
270                    log.warn("Recommended mode(s) were \"{}\" but available modes were \"{}\"", resetModeList, programmerModeList);
271                }
272            }
273        }
274        CvValue cv = rowVector.get(row);
275        log.debug("performReset: {}", cv);
276        _progState = WRITING_CV;
277        cv.write(_status);
278    }
279
280    @Override
281    public void actionPerformed(ActionEvent e) {
282        log.debug("action command: {}", e.getActionCommand());
283        char b = e.getActionCommand().charAt(0);
284        int row = Integer.parseInt(e.getActionCommand().substring(1));
285        log.debug("event on {} row {}", b, row);
286        if (b == 'W') {
287            // write command
288            performReset(row);
289        }
290    }
291
292    private int _progState = 0;
293    private static final int IDLE = 0;
294    private static final int WRITING_CV = 3;
295
296    @Override
297    public void propertyChange(PropertyChangeEvent e) {
298
299        log.debug("Property changed: {}", e.getPropertyName());
300        // notification from Indexed CV; check for Value being changed
301        if (e.getPropertyName().equals("Busy") && ((Boolean) e.getNewValue()).equals(Boolean.FALSE)) {
302            // busy transitions drive the state
303            switch (_progState) {
304                case IDLE:  // no, just an Indexed CV update
305                    log.debug("Busy goes false with state IDLE");
306                    return;
307                case WRITING_CV:  // now done with the write request
308                    log.debug("Finished writing the CV");
309                    mProgrammer.setMode(savedMode);
310                    _progState = IDLE;
311                    return;
312                default:  // unexpected!
313                    log.error("Unexpected state found: {}", _progState);
314                    mProgrammer.setMode(savedMode);
315                    _progState = IDLE;
316            }
317        }
318    }
319
320    /**
321     * Can provide some mechanism to prompt for user for one last chance to
322     * change his/her mind
323     * @param currentMode current programming mode
324     * @param resetModes representation of reset modes available
325     * @param availableModes representation of available modes
326     * @return true if user says to continue
327     */
328    boolean badModeOk(String currentMode, String resetModes, String availableModes) {
329        return true;
330    }
331
332    /**
333     * Can provide some mechanism to prompt for user for one last chance to
334     * change his/her mind
335     *
336     * @return true if user says to continue
337     */
338    boolean opsResetOk() {
339        return true;
340    }
341
342    public void dispose() {
343        log.debug("dispose");
344
345        // remove buttons
346        for (int i = 0; i < _writeButtons.size(); i++) {
347            _writeButtons.get(i).removeActionListener(this);
348        }
349
350        _writeButtons.clear();
351        _writeButtons = null;
352
353        // remove variables listeners
354        for (int i = 0; i < rowVector.size(); i++) {
355            CvValue cv = rowVector.get(i);
356            cv.dispose();
357        }
358        rowVector.clear();
359        rowVector = null;
360
361        labelVector.clear();
362        labelVector = null;
363
364        modeVector.clear();
365        modeVector = null;
366
367        headers = null;
368
369        _status = null;
370    }
371
372    // initialize logging
373    private final static Logger log = LoggerFactory.getLogger(ExtraMenuTableModel.class);
374}