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