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, 2025 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 DEVICENAME_COLUMN = 5; 039 public static final int ROSTERENTRY_COLUMN = 6; 040 public static final int OPENPRGMRBUTTON_COLUMN = 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(3); 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 DEVICENAME_COLUMN: 073 return Bundle.getMessage("HeadingDeviceModel"); 074 case ROSTERENTRY_COLUMN: 075 return Bundle.getMessage("HeadingDeviceId"); 076 case OPENPRGMRBUTTON_COLUMN: 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 OPENPRGMRBUTTON_COLUMN: 094 return javax.swing.JButton.class; 095 case DEVICENAME_COLUMN: 096 case ROSTERENTRY_COLUMN: 097 default: 098 return String.class; 099 } 100 } 101 102 @Override 103 public boolean isCellEditable(int r, int c) { 104 return (c == OPENPRGMRBUTTON_COLUMN); 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 DEVICENAME_COLUMN: 139 assert dev != null; 140 if (dev.getDeviceName().isEmpty()) { // 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.isEmpty()) { 158 for (DecoderFile d : l) { 159 // we do not check for LNCV programmingMode support 160 // we do not expect replies from non-LNCV devices 161 // TODO check using new access to getProgrammingModes() in the DecoderIndexFile 162 if (d.getModel().isEmpty()) { 163 log.warn("Empty model(name) in decoderfile {}", d.getFileName()); 164 continue; 165 } 166 lastModelName = d.getModel(); 167 } 168 dev.setDevName(lastModelName); 169 dev.setDecoderFile(l.get(l.size() - 1)); 170 } 171 return lastModelName; 172 } 173 return dev.getDeviceName(); 174 case ROSTERENTRY_COLUMN: 175 assert dev != null; 176 return dev.getRosterName(); 177 case OPENPRGMRBUTTON_COLUMN: 178 if (dev != null && !dev.getDeviceName().isEmpty()) { 179 if ((dev.getRosterName() != null) && (dev.getRosterName().isEmpty())) { 180 return Bundle.getMessage("ButtonCreateEntry"); 181 } 182 if (dev.getDecoderFile().isProgrammingMode("LOCONETLNCVMODE")) { 183 return Bundle.getMessage("ButtonProgram"); 184 } else { 185 return Bundle.getMessage("ButtonWrongMode"); 186 } 187 } 188 return Bundle.getMessage("ButtonNoMatchInRoster"); 189 default: // column 1 190 return r + 1; 191 } 192 } catch (NullPointerException npe) { 193 log.warn("Caught NPE reading Module {}", r); 194 return ""; 195 } 196 } 197 198 @Override 199 public void setValueAt(Object value, int r, int c) { 200 if (getRowCount() < r + 1) { 201 // prevent update of a row that does not (yet) exist 202 return; 203 } 204 LncvDevice dev = memo.getLncvDevicesManager().getDeviceList().getDevice(r); 205 if (c == OPENPRGMRBUTTON_COLUMN) { 206 if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonCreateEntry")) == 0) { 207 createRosterEntry(dev); 208 if (dev.getRosterEntry() != null) { 209 setValueAt(dev.getRosterName(), r, c); 210 } else { 211 log.warn("Failed to connect RosterEntry to device {}", dev.getRosterName()); 212 } 213 } else if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonProgram")) == 0) { 214 openProgrammer(r); 215 } else if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonWrongMode")) == 0) { 216 infoNotForLncv(getValueAt(r, 1).toString()); // TODO once we check for LNCV progMode this can be removed 217 } else if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonNoMatchInRoster")) == 0){ 218 // suggest to rebuild decoderIndex 219 warnRecreate(getValueAt(r, 1).toString()); 220 } 221 } else { 222 // no change, so do not fire a property change event 223 return; 224 } 225 if (getRowCount() >= 1) { 226 this.fireTableRowsUpdated(r, r); 227 } 228 } 229 230 private void openProgrammer(int r) { 231 LncvDevice dev = memo.getLncvDevicesManager().getDeviceList().getDevice(r); 232 233 LncvDevicesManager.ProgrammingResult result = lncvdm.prepareForSymbolicProgrammer(dev, this); 234 switch (result) { 235 case SUCCESS_PROGRAMMER_OPENED: 236 return; 237 case FAIL_NO_SUCH_DEVICE: 238 JmriJOptionPane.showMessageDialog(parent, 239 Bundle.getMessage("FAIL_NO_SUCH_DEVICE"), 240 Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE); 241 return; 242 case FAIL_NO_APPROPRIATE_PROGRAMMER: 243 JmriJOptionPane.showMessageDialog(parent, 244 Bundle.getMessage("FAIL_NO_APPROPRIATE_PROGRAMMER"), 245 Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE); 246 return; 247 case FAIL_NO_MATCHING_ROSTER_ENTRY: 248 JmriJOptionPane.showMessageDialog(parent, 249 Bundle.getMessage("FAIL_NO_MATCHING_ROSTER_ENTRY"), 250 Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE); 251 return; 252 case FAIL_DESTINATION_ADDRESS_IS_ZERO: 253 JmriJOptionPane.showMessageDialog(parent, 254 Bundle.getMessage("FAIL_DESTINATION_ADDRESS_IS_ZERO"), 255 Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE); 256 return; 257 case FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS: 258 JmriJOptionPane.showMessageDialog(parent, 259 Bundle.getMessage("FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS", dev.getDestAddr()), 260 Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE); 261 return; 262 case FAIL_NO_ADDRESSED_PROGRAMMER: 263 JmriJOptionPane.showMessageDialog(parent, 264 Bundle.getMessage("FAIL_NO_ADDRESSED_PROGRAMMER"), 265 Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE); 266 return; 267 case FAIL_NO_LNCV_PROGRAMMER: 268 JmriJOptionPane.showMessageDialog(parent, 269 Bundle.getMessage("FAIL_NO_LNCV_PROGRAMMER"), 270 Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE); 271 return; 272 default: 273 JmriJOptionPane.showMessageDialog(parent, 274 Bundle.getMessage("FAIL_UNKNOWN"), 275 Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE); 276 } 277 } 278 279 /** 280 * {@inheritDoc} 281 */ 282 @Override 283 public void openPaneOpsProgFrame(RosterEntry re, String name, 284 String programmerFile, Programmer p) { 285 // would be better if this was a new task on the GUI thread... 286 log.debug("attempting to open programmer, re={}, name={}, programmerFile={}, programmer={}", 287 re, name, programmerFile, p); 288 289 DecoderFile decoderFile = InstanceManager.getDefault(DecoderIndexFile.class).fileFromTitle(re.getDecoderModel()); 290 291 PaneOpsProgFrame progFrame = 292 new PaneOpsProgFrame(decoderFile, re, name, programmerFile, p); 293 294 progFrame.pack(); 295 progFrame.setVisible(true); 296 } 297 298 private void createRosterEntry(LncvDevice dev) { 299 if (dev.getDestAddr() == 0) { 300 JmriJOptionPane.showMessageDialog(parent, 301 Bundle.getMessage("FAIL_ADD_ENTRY_0"), 302 Bundle.getMessage("ButtonCreateEntry"), JmriJOptionPane.ERROR_MESSAGE); 303 } else { 304 String s = null; 305 while (s == null) { 306 s = JmriJOptionPane.showInputDialog(parent, 307 Bundle.getMessage("DialogEnterEntryName"), 308 Bundle.getMessage("EnterEntryNameTitle"),JmriJOptionPane.QUESTION_MESSAGE); 309 if (s == null) { 310 // Cancel button hit 311 return; 312 } 313 } 314 315 RosterEntry re = getRosterEntry(dev, s); 316 _roster.addEntry(re); 317 dev.setRosterEntry(re); 318 } 319 } 320 321 @Nonnull 322 private static RosterEntry getRosterEntry(LncvDevice dev, String s) { 323 RosterEntry re = new RosterEntry(dev.getDecoderFile().getFileName()); 324 re.setDccAddress(Integer.toString(dev.getDestAddr())); 325 re.setDecoderModel(dev.getDecoderFile().getModel()); 326 re.setProductID(Integer.toString(dev.getProductID())); 327 // add some details that won't be picked up otherwise from definition 328 re.setDecoderFamily(dev.getDecoderFile().getFileName()); 329 re.setProgrammingModes(dev.getDecoderFile().getProgrammingModes()); 330 re.setId(s); 331 return re; 332 } 333 334 private void warnRecreate(String address) { 335 // show dialog to inform and allow rebuilding index 336 Object[] dialogBoxButtonOptions = { 337 Bundle.getMessage("ButtonRecreateIndex"), 338 Bundle.getMessage("ButtonCancel")}; 339 int userReply = JmriJOptionPane.showOptionDialog(parent, 340 Bundle.getMessage("DialogWarnRecreate", address), 341 Bundle.getMessage("TitleOpenRosterEntry"), 342 JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, 343 null, dialogBoxButtonOptions, dialogBoxButtonOptions[0]); 344 if (userReply == 0) { // array position 0 345 try { 346 DecoderIndexFile.forceCreationOfNewIndex(false); // faster 347 } catch (Exception exq) { 348 log.error("exception updating decoderIndexFile", exq); 349 } 350 } 351 } 352 353 /** 354 * Show dialog to inform that address matched decoder doesn't support LNSV1 mode. 355 */ 356 private void infoNotForLncv(String address) { 357 Object[] dialogBoxButtonOptions = { 358 Bundle.getMessage("ButtonOK")}; 359 JmriJOptionPane.showOptionDialog(parent, 360 Bundle.getMessage("DialogInfoMatchNotX", address, "LNCV"), 361 Bundle.getMessage("TitleOpenRosterEntry"), 362 JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, 363 null, dialogBoxButtonOptions, dialogBoxButtonOptions[0]); 364 } 365 366 /* 367 * Process the "property change" events from LncvDevicesManager. 368 * 369 * @param evt event 370 */ 371 @Override 372 public void propertyChange(PropertyChangeEvent evt) { 373 // these messages can arrive without a complete 374 // GUI, in which case we just ignore them 375 //String eventName = evt.getPropertyName(); 376 /* always use fireTableDataChanged() because it does not always 377 resize columns to "preferred" widths! 378 This may slow things down, but that is a small price to pay! 379 */ 380 fireTableDataChanged(); 381 } 382 383 public void dispose() { 384 if ((memo != null) && (memo.getLncvDevicesManager() != null)) { 385 memo.getLncvDevicesManager().removePropertyChangeListener(this); 386 } 387 } 388 389 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LncvProgTableModel.class); 390 391}