001package jmri.jmrix.loconet.swing.lncvprog;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.List;
006
007import javax.annotation.Nonnull;
008import javax.swing.table.AbstractTableModel;
009import javax.swing.table.TableColumn;
010import javax.swing.table.TableColumnModel;
011
012import jmri.InstanceManager;
013import jmri.Programmer;
014import jmri.jmrit.decoderdefn.DecoderFile;
015import jmri.jmrit.decoderdefn.DecoderIndexFile;
016import jmri.jmrit.roster.Roster;
017import jmri.jmrit.roster.RosterEntry;
018import jmri.jmrit.symbolicprog.tabbedframe.PaneOpsProgFrame;
019import jmri.jmrix.ProgrammingTool;
020import jmri.jmrix.loconet.LncvDevicesManager;
021import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
022import jmri.jmrix.loconet.uhlenbrock.LncvDevice;
023import jmri.util.swing.JmriJOptionPane;
024
025/**
026 * Table model for the programmed LNCV Modules table.
027 * See Sv2f Programing tool
028 *
029 * @author Egbert Broerse Copyright (C) 2020
030 */
031public class LncvProgTableModel extends AbstractTableModel implements PropertyChangeListener, ProgrammingTool {
032
033    public static final int COUNT_COLUMN = 0;
034    public static final int ARTICLE_COLUMN = 1;
035    public static final int MODADDR_COLUMN = 2;
036    public static final int CV_COLUMN = 3;
037    public static final int VALUE_COLUMN = 4;
038    public static final int DEVICENAMECOLUMN = 5;
039    public static final int ROSTERENTRYCOLUMN = 6;
040    public static final int OPENPRGMRBUTTONCOLUMN = 7;
041    static public final int NUMCOLUMNS = 8;
042    private final LncvProgPane parent;
043    private final transient LocoNetSystemConnectionMemo memo;
044    protected Roster _roster;
045    protected LncvDevicesManager lncvdm;
046
047    LncvProgTableModel(LncvProgPane parent, @Nonnull LocoNetSystemConnectionMemo memo) {
048        this.parent = parent;
049        this.memo = memo;
050        lncvdm = memo.getLncvDevicesManager();
051        _roster = Roster.getDefault();
052        lncvdm.addPropertyChangeListener(this);
053    }
054
055    public void initTable(javax.swing.JTable lncvModulesTable) {
056       TableColumnModel assignmentColumnModel = lncvModulesTable.getColumnModel();
057       TableColumn idColumn = assignmentColumnModel.getColumn(0);
058       idColumn.setMaxWidth(8);
059    }
060
061   @Override
062   public String getColumnName(int c) {
063       switch (c) {
064           case ARTICLE_COLUMN:
065               return Bundle.getMessage("HeadingArticle");
066           case MODADDR_COLUMN:
067               return Bundle.getMessage("HeadingAddress");
068           case CV_COLUMN:
069               return Bundle.getMessage("HeadingCvLastRead");
070           case VALUE_COLUMN:
071               return Bundle.getMessage("HeadingValue");
072           case DEVICENAMECOLUMN:
073               return Bundle.getMessage("HeadingDeviceModel");
074           case ROSTERENTRYCOLUMN:
075               return Bundle.getMessage("HeadingDeviceId");
076           case OPENPRGMRBUTTONCOLUMN:
077               return Bundle.getMessage("ButtonProgram");
078           case COUNT_COLUMN:
079           default:
080               return "#";
081       }
082   }
083
084   @Override
085   public Class<?> getColumnClass(int c) {
086       switch (c) {
087           case COUNT_COLUMN:
088           case ARTICLE_COLUMN:
089           case MODADDR_COLUMN:
090           case CV_COLUMN:
091           case VALUE_COLUMN:
092               return Integer.class;
093           case OPENPRGMRBUTTONCOLUMN:
094               return javax.swing.JButton.class;
095           case DEVICENAMECOLUMN:
096           case ROSTERENTRYCOLUMN:
097           default:
098               return String.class;
099       }
100   }
101
102   @Override
103   public boolean isCellEditable(int r, int c) {
104       return (c == OPENPRGMRBUTTONCOLUMN);
105   }
106
107   @Override
108   public int getColumnCount() {
109      return NUMCOLUMNS;
110   }
111
112   @Override
113   public int getRowCount() {
114        if (lncvdm == null) {
115            return 0;
116        } else {
117            return lncvdm.getDeviceCount();
118        }
119   }
120
121   @Override
122   public Object getValueAt(int r, int c) {
123       LncvDevice dev = memo.getLncvDevicesManager().getDeviceList().getDevice(r);
124       try {
125          switch (c) {
126              case ARTICLE_COLUMN:
127                  assert dev != null;
128                  return dev.getProductID();
129              case MODADDR_COLUMN:
130                  assert dev != null;
131                  return dev.getDestAddr();
132              case CV_COLUMN:
133                  assert dev != null;
134                  return dev.getCvNum();
135              case VALUE_COLUMN:
136                  assert dev != null;
137                  return dev.getCvValue();
138              case DEVICENAMECOLUMN:
139                  assert dev != null;
140                  if (dev.getDeviceName().length() == 0) { // not yet filled in, look for a candidate
141                      List<DecoderFile> l =
142                          InstanceManager.getDefault(
143                              DecoderIndexFile.class).
144                              matchingDecoderList(
145                                      null,
146                                      null,
147                                      null,
148                                      null,
149                                      String.valueOf(dev.getProductID()), // a bit risky to check just 1 value
150                                      null,
151                                      null,
152                                      null,
153                                      null
154                              );
155                      //log.debug("found {} possible decoder matches for LNCV device", l.size());
156                      String lastModelName = "";
157                      if (l.size() > 0) {
158                          for (DecoderFile d : l) {
159                              // we do not check for LNCV programmingMode support since we do not expect replies from non-LNCV devices
160                              // (and there is currently no access to supported modes in the DecoderIndexFile)
161                              if (d.getModel().equals("")) {
162                                  log.warn("Empty model(name) in decoderfile {}", d.getFileName());
163                                  continue;
164                              }
165                              lastModelName = d.getModel();
166                          }
167                          dev.setDevName(lastModelName);
168                          dev.setDecoderFile(l.get(l.size() - 1));
169                      }
170                      return lastModelName;
171                  }
172                  return dev.getDeviceName();
173              case ROSTERENTRYCOLUMN:
174                  assert dev != null;
175                  return dev.getRosterName();
176              case OPENPRGMRBUTTONCOLUMN:
177                  assert dev != null;
178                  if (dev.getDeviceName().length() != 0) {
179                      if ((dev.getRosterName() != null) && (dev.getRosterName().length() == 0)) {
180                          return Bundle.getMessage("ButtonCreateEntry");
181                      }
182                      return Bundle.getMessage("ButtonProgram");
183                  }
184                  return Bundle.getMessage("ButtonNoMatchInRoster");
185              default: // column 1
186                 return r + 1;
187          }
188      } catch (NullPointerException npe) {
189        log.warn("Caught NPE reading Module {}", r);
190        return "";
191      }
192   }
193
194    @Override
195    public void setValueAt(Object value, int r, int c) {
196        if (getRowCount() < r + 1) {
197            // prevent update of a row that does not (yet) exist
198            return;
199        }
200        LncvDevice dev = memo.getLncvDevicesManager().getDeviceList().getDevice(r);
201        if (c == OPENPRGMRBUTTONCOLUMN) {
202            if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonCreateEntry")) == 0) {
203                createRosterEntry(dev);
204                if (dev.getRosterEntry() != null) {
205                    setValueAt(dev.getRosterName(), r, c);
206                } else {
207                    log.warn("Failed to connect RosterEntry to device {}", dev.getRosterName());
208                }
209            } else if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonProgram")) == 0) {
210                openProgrammer(r);
211            } else if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonNoMatchInRoster")) == 0){
212                // need to rebuild decoderIndex, tooltip?
213                warnRecreate();
214            }
215        } else {
216            // no change, so do not fire a property change event
217            return;
218        }
219        if (getRowCount() >= 1) {
220            this.fireTableRowsUpdated(r, r);
221        }
222    }
223
224    private void openProgrammer(int r) {
225        LncvDevice dev = memo.getLncvDevicesManager().getDeviceList().getDevice(r);
226
227        LncvDevicesManager.ProgrammingResult result = lncvdm.prepareForSymbolicProgrammer(dev, this);
228        switch (result) {
229            case SUCCESS_PROGRAMMER_OPENED:
230                return;
231            case FAIL_NO_SUCH_DEVICE:
232                JmriJOptionPane.showMessageDialog(parent,
233                        Bundle.getMessage("FAIL_NO_SUCH_DEVICE"),
234                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
235                return;
236            case FAIL_NO_APPROPRIATE_PROGRAMMER:
237                JmriJOptionPane.showMessageDialog(parent,
238                        Bundle.getMessage("FAIL_NO_APPROPRIATE_PROGRAMMER"),
239                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
240                return;
241            case FAIL_NO_MATCHING_ROSTER_ENTRY:
242                JmriJOptionPane.showMessageDialog(parent,
243                        Bundle.getMessage("FAIL_NO_MATCHING_ROSTER_ENTRY"),
244                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
245                return;
246            case FAIL_DESTINATION_ADDRESS_IS_ZERO:
247                JmriJOptionPane.showMessageDialog(parent,
248                        Bundle.getMessage("FAIL_DESTINATION_ADDRESS_IS_ZERO"),
249                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
250                return;
251            case FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS:
252                JmriJOptionPane.showMessageDialog(parent,
253                        Bundle.getMessage("FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS", dev.getDestAddr()),
254                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
255                return;
256            case FAIL_NO_ADDRESSED_PROGRAMMER:
257                JmriJOptionPane.showMessageDialog(parent,
258                        Bundle.getMessage("FAIL_NO_ADDRESSED_PROGRAMMER"),
259                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
260                return;
261            case FAIL_NO_LNCV_PROGRAMMER:
262                JmriJOptionPane.showMessageDialog(parent,
263                        Bundle.getMessage("FAIL_NO_LNCV_PROGRAMMER"),
264                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
265                return;
266            default:
267                JmriJOptionPane.showMessageDialog(parent,
268                        Bundle.getMessage("FAIL_UNKNOWN"),
269                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
270        }
271    }
272
273    /**
274     * {@inheritDoc}
275     */
276    @Override
277    public void openPaneOpsProgFrame(RosterEntry re, String name,
278                                     String programmerFile, Programmer p) {
279        // would be better if this was a new task on the GUI thread...
280        log.debug("attempting to open programmer, re={}, name={}, programmerFile={}, programmer={}",
281                re, name, programmerFile, p);
282
283        DecoderFile decoderFile = InstanceManager.getDefault(DecoderIndexFile.class).fileFromTitle(re.getDecoderModel());
284
285        PaneOpsProgFrame progFrame =
286                new PaneOpsProgFrame(decoderFile, re, name, programmerFile, p);
287
288        progFrame.pack();
289        progFrame.setVisible(true);
290    }
291
292    private void createRosterEntry(LncvDevice dev) {
293        if (dev.getDestAddr() == 0) {
294            JmriJOptionPane.showMessageDialog(parent,
295                    Bundle.getMessage("FAIL_ADD_ENTRY_0"),
296                    Bundle.getMessage("ButtonCreateEntry"), JmriJOptionPane.ERROR_MESSAGE);
297        } else {
298            String s = null;
299            while (s == null) {
300                s = JmriJOptionPane.showInputDialog(parent,
301                        Bundle.getMessage("DialogEnterEntryName"),
302                        Bundle.getMessage("EnterEntryNameTitle"),JmriJOptionPane.QUESTION_MESSAGE);
303                if (s == null) {
304                    // Cancel button hit
305                    return;
306                }
307            }
308
309            RosterEntry re = new RosterEntry(dev.getDecoderFile().getFileName());
310            re.setDccAddress(Integer.toString(dev.getDestAddr()));
311            re.setDecoderModel(dev.getDecoderFile().getModel());
312            re.setProductID(Integer.toString(dev.getProductID()));
313            re.setId(s);
314            _roster.addEntry(re);
315            dev.setRosterEntry(re);
316        }
317    }
318
319    private void warnRecreate() {
320        // show dialog to inform and allow rebuilding index
321        Object[] dialogBoxButtonOptions = {
322                Bundle.getMessage("ButtonRecreateIndex"),
323                Bundle.getMessage("ButtonCancel")};
324        int userReply = JmriJOptionPane.showOptionDialog(parent,
325                Bundle.getMessage("DialogWarnRecreate"),
326                Bundle.getMessage("TitleOpenRosterEntry"),
327                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE,
328                null, dialogBoxButtonOptions, dialogBoxButtonOptions[0]);
329        if (userReply == 0) { // array position 0
330            DecoderIndexFile.forceCreationOfNewIndex(false); // faster
331        }
332    }
333
334    /*
335     * Process the "property change" events from LncvDevicesManager.
336     *
337     * @param evt event
338     */
339    @Override
340    public void propertyChange(PropertyChangeEvent evt) {
341        // these messages can arrive without a complete
342        // GUI, in which case we just ignore them
343        //String eventName = evt.getPropertyName();
344        /* always use fireTableDataChanged() because it does not always
345            resize columns to "preferred" widths!
346            This may slow things down, but that is a small price to pay!
347        */
348        fireTableDataChanged();
349    }
350
351    public void dispose() {
352        if ((memo != null) && (memo.getLncvDevicesManager() != null)) {
353            memo.getLncvDevicesManager().removePropertyChangeListener(this);
354        }
355    }
356
357    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LncvProgTableModel.class);
358
359}