001package jmri.jmrit.roster.swing; 002 003import com.fasterxml.jackson.databind.util.StdDateFormat; 004 005import java.beans.PropertyChangeEvent; 006import java.beans.PropertyChangeListener; 007import java.text.ParseException; 008import java.util.*; 009 010import javax.annotation.CheckForNull; 011import javax.swing.Icon; 012import javax.swing.ImageIcon; 013import javax.swing.JLabel; 014import javax.swing.JTable; 015import javax.swing.RowSorter; 016import javax.swing.table.DefaultTableModel; 017 018import jmri.InstanceManager; 019import jmri.jmrit.decoderdefn.DecoderIndexFile; 020import jmri.jmrit.roster.Roster; 021import jmri.jmrit.roster.RosterEntry; 022import jmri.jmrit.roster.RosterIconFactory; 023import jmri.jmrit.roster.rostergroup.RosterGroup; 024import jmri.jmrit.roster.rostergroup.RosterGroupSelector; 025import jmri.util.gui.GuiLafPreferencesManager; 026 027/** 028 * Table data model for display of Roster variable values. 029 * <p> 030 * Any desired ordering, etc, is handled outside this class. 031 * <p> 032 * The initial implementation doesn't automatically update when roster entries 033 * change, doesn't allow updating of the entries, and only shows some of the 034 * fields. But it's a start.... 035 * 036 * @author Bob Jacobsen Copyright (C) 2009, 2010 037 * @since 2.7.5 038 */ 039public class RosterTableModel extends DefaultTableModel implements PropertyChangeListener { 040 041 public static final int IDCOL = 0; 042 static final int ADDRESSCOL = 1; 043 static final int ICONCOL = 2; 044 static final int DECODERMFGCOL = 3; 045 static final int DECODERFAMILYCOL = 4; 046 static final int DECODERMODELCOL = 5; 047 static final int ROADNAMECOL = 6; 048 static final int ROADNUMBERCOL = 7; 049 static final int MFGCOL = 8; 050 static final int MODELCOL = 9; 051 static final int OWNERCOL = 10; 052 static final int DATEUPDATECOL = 11; 053 public static final int PROTOCOL = 12; 054 static final int COMMENT = 13; 055 public static final int NUMCOL = COMMENT + 1; 056 private String rosterGroup = null; 057 boolean editable = false; 058 059 public RosterTableModel() { 060 this(false); 061 } 062 063 public RosterTableModel(boolean editable) { 064 this.editable = editable; 065 Roster.getDefault().addPropertyChangeListener(RosterTableModel.this); 066 setRosterGroup(null); // add prop change listeners to roster entries 067 } 068 069 /** 070 * Create a table model for a Roster group. 071 * 072 * @param group the roster group to show; if null, behaves the same as 073 * {@link #RosterTableModel()} 074 */ 075 public RosterTableModel(@CheckForNull RosterGroup group) { 076 this(false); 077 if (group != null) { 078 this.setRosterGroup(group.getName()); 079 } 080 } 081 082 JTable associatedTable; 083 public void setAssociatedTable(JTable associatedTable) { 084 this.associatedTable = associatedTable; 085 } 086 087 RowSorter<RosterTableModel> associatedSorter; 088 public void setAssociatedSorter(RowSorter<RosterTableModel> associatedSorter) { 089 this.associatedSorter = associatedSorter; 090 } 091 092 093 @Override 094 public void propertyChange(PropertyChangeEvent e) { 095 if (e.getPropertyName().equals(Roster.ADD)) { 096 setRosterGroup(getRosterGroup()); // add prop change listener to new entry 097 fireTableDataChanged(); 098 } else if (e.getPropertyName().equals(Roster.REMOVE)) { 099 fireTableDataChanged(); 100 } else if (e.getPropertyName().equals(Roster.SAVED)) { 101 //TODO This really needs to do something like find the index of the roster entry here 102 if (e.getSource() instanceof RosterEntry) { 103 int row = Roster.getDefault().getGroupIndex(rosterGroup, (RosterEntry) e.getSource()); 104 fireTableRowsUpdated(row, row); 105 } else { 106 fireTableDataChanged(); 107 } 108 } else if (e.getPropertyName().equals(RosterGroupSelector.SELECTED_ROSTER_GROUP)) { 109 setRosterGroup((e.getNewValue() != null) ? e.getNewValue().toString() : null); 110 } else if (e.getPropertyName().startsWith("attribute") && e.getSource() instanceof RosterEntry) { // NOI18N 111 int row = Roster.getDefault().getGroupIndex(rosterGroup, (RosterEntry) e.getSource()); 112 fireTableRowsUpdated(row, row); 113 } else if (e.getPropertyName().equals(Roster.ROSTER_GROUP_ADDED) && e.getNewValue().equals(rosterGroup)) { 114 fireTableDataChanged(); 115 } 116 } 117 118 @Override 119 public int getRowCount() { 120 return Roster.getDefault().numGroupEntries(rosterGroup); 121 } 122 123 @Override 124 public int getColumnCount() { 125 return NUMCOL + getModelAttributeKeyColumnNames().length; 126 } 127 128 @Override 129 public String getColumnName(int col) { 130 switch (col) { 131 case IDCOL: 132 return Bundle.getMessage("FieldID"); 133 case ADDRESSCOL: 134 return Bundle.getMessage("FieldDCCAddress"); 135 case DECODERMFGCOL: 136 return Bundle.getMessage("FieldDecoderMfg"); 137 case DECODERFAMILYCOL: 138 return Bundle.getMessage("FieldDecoderFamily"); 139 case DECODERMODELCOL: 140 return Bundle.getMessage("FieldDecoderModel"); 141 case MODELCOL: 142 return Bundle.getMessage("FieldModel"); 143 case ROADNAMECOL: 144 return Bundle.getMessage("FieldRoadName"); 145 case ROADNUMBERCOL: 146 return Bundle.getMessage("FieldRoadNumber"); 147 case MFGCOL: 148 return Bundle.getMessage("FieldManufacturer"); 149 case ICONCOL: 150 return Bundle.getMessage("FieldIcon"); 151 case OWNERCOL: 152 return Bundle.getMessage("FieldOwner"); 153 case DATEUPDATECOL: 154 return Bundle.getMessage("FieldDateUpdated"); 155 case PROTOCOL: 156 return Bundle.getMessage("FieldProtocol"); 157 case COMMENT: 158 return Bundle.getMessage("FieldComment"); 159 default: 160 return getColumnNameAttribute(col); 161 } 162 } 163 164 private String getColumnNameAttribute(int col) { 165 if ( col < getColumnCount() ) { 166 String attributeKey = getAttributeKey(col); 167 try { 168 return Bundle.getMessage(attributeKey); 169 } catch (java.util.MissingResourceException ex){} 170 171 String[] r = attributeKey.split("(?=\\p{Lu})"); // NOI18N 172 StringBuilder sb = new StringBuilder(); 173 sb.append(r[0].trim()); 174 for (int j = 1; j < r.length; j++) { 175 sb.append(" "); 176 sb.append(r[j].trim()); 177 } 178 return sb.toString(); 179 } 180 return "<UNKNOWN>"; // NOI18N 181 } 182 183 @Override 184 public Class<?> getColumnClass(int col) { 185 switch (col) { 186 case ADDRESSCOL: 187 return Integer.class; 188 case ICONCOL: 189 return ImageIcon.class; 190 case DATEUPDATECOL: 191 return Date.class; 192 default: 193 return getColumnClassAttribute(col); 194 } 195 } 196 197 private Class<?> getColumnClassAttribute(int col){ 198 if (RosterEntry.ATTRIBUTE_LAST_OPERATED.equals( getAttributeKey(col))) { 199 return Date.class; 200 } 201 if (RosterEntry.ATTRIBUTE_OPERATING_DURATION.equals( getAttributeKey(col))) { 202 return Integer.class; 203 } 204 return String.class; 205 } 206 207 /** 208 * {@inheritDoc} 209 * <p> 210 * Note that the table can be set to be non-editable when constructed, in 211 * which case this always returns false. 212 * 213 * @return true if cell is editable in roster entry model and table allows 214 * editing 215 */ 216 @Override 217 public boolean isCellEditable(int row, int col) { 218 if (col == ADDRESSCOL) { 219 return false; 220 } 221 if (col == PROTOCOL) { 222 return false; 223 } 224 if (col == DECODERMFGCOL) { 225 return false; 226 } 227 if (col == DECODERFAMILYCOL) { 228 return false; 229 } 230 if (col == DECODERMODELCOL) { 231 return false; 232 } 233 if (col == ICONCOL) { 234 return false; 235 } 236 if (col == DATEUPDATECOL) { 237 return false; 238 } 239 if (editable) { 240 RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row); 241 if (re != null) { 242 return (!re.isOpen()); 243 } 244 } 245 return editable; 246 } 247 248 RosterIconFactory iconFactory = null; 249 250 ImageIcon getIcon(RosterEntry re) { 251 // defer image handling to RosterIconFactory 252 if (iconFactory == null) { 253 iconFactory = new RosterIconFactory(Math.max(19, new JLabel(getColumnName(0)).getPreferredSize().height)); 254 } 255 return iconFactory.getIcon(re); 256 } 257 258 /** 259 * {@inheritDoc} 260 * 261 * Provides an empty string for a column if the model returns null for that 262 * value. 263 */ 264 @Override 265 public Object getValueAt(int row, int col) { 266 // get roster entry for row 267 RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row); 268 if (re == null) { 269 log.debug("roster entry is null!"); 270 return null; 271 } 272 switch (col) { 273 case IDCOL: 274 return re.getId(); 275 case ADDRESSCOL: 276 return re.getDccLocoAddress().getNumber(); 277 case DECODERMFGCOL: 278 var index = InstanceManager.getDefault(DecoderIndexFile.class); 279 var matches = index.matchingDecoderList( 280 null, re.getDecoderFamily(), 281 null, null, null, 282 re.getDecoderModel() 283 ); 284 if (matches.size() == 0) return ""; 285 return matches.get(0).getMfg(); 286 case DECODERFAMILYCOL: 287 return re.getDecoderFamily(); 288 case DECODERMODELCOL: 289 return re.getDecoderModel(); 290 case MODELCOL: 291 return re.getModel(); 292 case ROADNAMECOL: 293 return re.getRoadName(); 294 case ROADNUMBERCOL: 295 return re.getRoadNumber(); 296 case MFGCOL: 297 return re.getMfg(); 298 case ICONCOL: 299 return getIcon(re); 300 case OWNERCOL: 301 return re.getOwner(); 302 case DATEUPDATECOL: 303 // will not display last update if not parsable as date 304 return re.getDateModified(); 305 case PROTOCOL: 306 return re.getProtocolAsString(); 307 case COMMENT: 308 // have to set height for extra lines 309 var viewRow = associatedSorter.convertRowIndexToView(row); 310 String[] sections = re.getComment().split("\n"); 311 int lines = sections.length; 312 int height = lines * (InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize() + 4); // same line height as in RosterTable 313 associatedTable.setRowHeight(viewRow, height); 314 return re.getComment(); 315 default: 316 break; 317 } 318 return getValueAtAttribute(re, col); 319 } 320 321 private Object getValueAtAttribute(RosterEntry re, int col){ 322 String attributeKey = getAttributeKey(col); 323 String value = re.getAttribute(attributeKey); // NOI18N 324 if (RosterEntry.ATTRIBUTE_LAST_OPERATED.equals( attributeKey)) { 325 if (value == null){ 326 return null; 327 } 328 try { 329 return new StdDateFormat().parse(value); 330 } catch (ParseException ex){ 331 return null; 332 } 333 } 334 if ( RosterEntry.ATTRIBUTE_OPERATING_DURATION.equals( attributeKey) ) { 335 try { 336 return Integer.valueOf(value); 337 } 338 catch (NumberFormatException e) { 339 log.debug("could not format duration ( String integer of total seconds ) in {}", value, e); 340 } 341 return 0; 342 } 343 return (value == null ? "" : value); 344 } 345 346 @Override 347 public void setValueAt(Object value, int row, int col) { 348 // get roster entry for row 349 RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row); 350 if (re == null) { 351 log.warn("roster entry is null!"); 352 return; 353 } 354 if (re.isOpen()) { 355 log.warn("Entry is already open"); 356 return; 357 } 358 if (Objects.equals(value, getValueAt(row, col))) { 359 return; 360 } 361 String valueToSet = (String) value; 362 switch (col) { 363 case IDCOL: 364 re.setId(valueToSet); 365 break; 366 case ROADNAMECOL: 367 re.setRoadName(valueToSet); 368 break; 369 case ROADNUMBERCOL: 370 re.setRoadNumber(valueToSet); 371 break; 372 case MFGCOL: 373 re.setMfg(valueToSet); 374 break; 375 case MODELCOL: 376 re.setModel(valueToSet); 377 break; 378 case OWNERCOL: 379 re.setOwner(valueToSet); 380 break; 381 case COMMENT: 382 re.setComment(valueToSet); 383 break; 384 default: 385 setValueAtAttribute(valueToSet, re, col); 386 break; 387 } 388 // need to mark as updated 389 re.changeDateUpdated(); 390 re.updateFile(); 391 } 392 393 private void setValueAtAttribute(String valueToSet, RosterEntry re, int col) { 394 String attributeKey = getAttributeKey(col); 395 if ((valueToSet == null) || valueToSet.isEmpty()) { 396 re.deleteAttribute(attributeKey); 397 } else { 398 re.putAttribute(attributeKey, valueToSet); 399 } 400 } 401 402 public int getPreferredWidth(int column) { 403 int retval = 20; // always take some width 404 retval = Math.max(retval, new JLabel(getColumnName(column)) 405 .getPreferredSize().width + 15); // leave room for sorter arrow 406 for (int row = 0; row < getRowCount(); row++) { 407 if (getColumnClass(column).equals(String.class)) { 408 retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width); 409 } else if (getColumnClass(column).equals(Integer.class)) { 410 retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width); 411 } else if (getColumnClass(column).equals(ImageIcon.class)) { 412 retval = Math.max(retval, new JLabel((Icon) getValueAt(row, column)).getPreferredSize().width); 413 } 414 } 415 return retval + 5; 416 } 417 418 public final void setRosterGroup(String rosterGroup) { 419 Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach( re -> 420 re.removePropertyChangeListener(this)); 421 this.rosterGroup = rosterGroup; 422 Roster.getDefault().getEntriesInGroup(rosterGroup).forEach( re -> 423 re.addPropertyChangeListener(this)); 424 fireTableDataChanged(); 425 } 426 427 public final String getRosterGroup() { 428 return this.rosterGroup; 429 } 430 431 // access via method to ensure not null 432 private String[] attributeKeys = null; 433 434 private String[] getModelAttributeKeyColumnNames() { 435 if ( attributeKeys == null ) { 436 Set<String> result = new TreeSet<>(); 437 for (String s : Roster.getDefault().getAllAttributeKeys()) { 438 if ( !s.contains("RosterGroup") 439 && !s.toLowerCase().startsWith("sys") 440 && !s.toUpperCase().startsWith("VSD")) { // NOI18N 441 result.add(s); 442 } 443 } 444 attributeKeys = result.toArray(String[]::new); 445 } 446 return attributeKeys; 447 } 448 449 private String getAttributeKey(int col) { 450 if ( col >= NUMCOL && col < getColumnCount() ) { 451 return getModelAttributeKeyColumnNames()[col - NUMCOL ]; 452 } 453 return ""; 454 } 455 456 // drop listeners 457 public void dispose() { 458 Roster.getDefault().removePropertyChangeListener(this); 459 Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach( re -> 460 re.removePropertyChangeListener(this) ); 461 } 462 463 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterTableModel.class); 464 465}