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 = 1; 330 331 var columnModel = (jmri.util.swing.XTableColumnModel) associatedTable.getColumnModel(); 332 boolean visible = columnModel.isColumnVisible(columnModel.getColumnByModelIndex(RosterTableModel.COMMENT)); 333 if (visible) { 334 lines = Math.max(lines, countLinesIn(re.getComment())); 335 } 336 337 String[] auxAttributeNames = getModelAttributeKeyColumnNames(); 338 for (String attributeKey : auxAttributeNames) { 339 String value = re.getAttribute(attributeKey); 340 if (value != null) { 341 int index = getAttributeColumn(attributeKey); 342 visible = columnModel.isColumnVisible(columnModel.getColumnByModelIndex(index)); 343 344 int count = countLinesIn(value); 345 if (visible) { 346 lines = Math.max(lines, count); 347 } 348 } 349 } 350 return lines; 351 } 352 353 int countLinesIn(String text) { 354 String[] sections = text.split("\n"); 355 int lines = sections.length; 356 return lines; 357 } 358 359 @Override 360 public void resizeRowToText(int modelRow, int heightInLines) { 361 if (associatedSorter == null || associatedTable == null ) { 362 return; // because not initialized, can't act - useful for tests 363 } 364 365 if (heightInLines < 1) heightInLines = 1; // always show at least one line 366 367 var viewRow = associatedSorter.convertRowIndexToView(modelRow); 368 int height = heightInLines * (InstanceManager.getDefault(GuiLafPreferencesManager.class).getFontSize() + 4); // same line height as in RosterTable 369 if (height != associatedTable.getRowHeight(viewRow)) { 370 associatedTable.setRowHeight(viewRow, height); 371 } 372 } 373 374 private Object getValueAtAttribute(RosterEntry re, int col){ 375 String attributeKey = getAttributeKey(col); 376 String value = re.getAttribute(attributeKey); // NOI18N 377 if (RosterEntry.ATTRIBUTE_LAST_OPERATED.equals( attributeKey)) { 378 if (value == null){ 379 return null; 380 } 381 try { 382 return new StdDateFormat().parse(value); 383 } catch (ParseException ex){ 384 return null; 385 } 386 } 387 if ( RosterEntry.ATTRIBUTE_OPERATING_DURATION.equals( attributeKey) ) { 388 try { 389 return Integer.valueOf(value); 390 } 391 catch (NumberFormatException e) { 392 log.debug("could not format duration ( String integer of total seconds ) in {}", value, e); 393 } 394 return 0; 395 } 396 return (value == null ? "" : value); 397 } 398 399 @Override 400 public void setValueAt(Object value, int row, int col) { 401 // get roster entry for row 402 RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row); 403 if (re == null) { 404 log.warn("roster entry is null!"); 405 return; 406 } 407 if (re.isOpen()) { 408 log.warn("Entry is already open"); 409 return; 410 } 411 if (Objects.equals(value, getValueAt(row, col))) { 412 return; 413 } 414 String valueToSet = (String) value; 415 switch (col) { 416 case IDCOL: 417 re.setId(valueToSet); 418 break; 419 case ROADNAMECOL: 420 re.setRoadName(valueToSet); 421 break; 422 case ROADNUMBERCOL: 423 re.setRoadNumber(valueToSet); 424 break; 425 case MFGCOL: 426 re.setMfg(valueToSet); 427 break; 428 case MODELCOL: 429 re.setModel(valueToSet); 430 break; 431 case OWNERCOL: 432 re.setOwner(valueToSet); 433 break; 434 case COMMENT: 435 re.setComment(valueToSet); 436 break; 437 default: 438 // permission to edit optional columns? 439 if (! permissionManager.ensureAtLeastPermission(PermissionsProgrammer.PERMISSION_ROSTER_ADDED_COLUMNS, 440 BooleanPermission.BooleanValue.TRUE)) { 441 return; 442 } 443 444 setValueAtAttribute(valueToSet, re, col); 445 break; 446 } 447 // need to mark as updated 448 re.changeDateUpdated(); 449 re.updateFile(); 450 } 451 452 private void setValueAtAttribute(String valueToSet, RosterEntry re, int col) { 453 String attributeKey = getAttributeKey(col); 454 if ((valueToSet == null) || valueToSet.isEmpty()) { 455 re.deleteAttribute(attributeKey); 456 } else { 457 re.putAttribute(attributeKey, valueToSet); 458 } 459 } 460 461 public int getPreferredWidth(int column) { 462 int retval = 20; // always take some width 463 retval = Math.max(retval, new JLabel(getColumnName(column)) 464 .getPreferredSize().width + 15); // leave room for sorter arrow 465 for (int row = 0; row < getRowCount(); row++) { 466 if (getColumnClass(column).equals(String.class)) { 467 retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width); 468 } else if (getColumnClass(column).equals(Integer.class)) { 469 retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width); 470 } else if (getColumnClass(column).equals(ImageIcon.class)) { 471 retval = Math.max(retval, new JLabel((Icon) getValueAt(row, column)).getPreferredSize().width); 472 } 473 } 474 return retval + 5; 475 } 476 477 public final void setRosterGroup(String rosterGroup) { 478 Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach( re -> 479 re.removePropertyChangeListener(this)); 480 this.rosterGroup = rosterGroup; 481 Roster.getDefault().getEntriesInGroup(rosterGroup).forEach( re -> 482 re.addPropertyChangeListener(this)); 483 fireTableDataChanged(); 484 } 485 486 public final String getRosterGroup() { 487 return this.rosterGroup; 488 } 489 490 // access via method to ensure not null 491 private String[] attributeKeys = null; 492 493 private String[] getModelAttributeKeyColumnNames() { 494 if ( attributeKeys == null ) { 495 Set<String> result = new TreeSet<>(); 496 for (String s : Roster.getDefault().getAllAttributeKeys()) { 497 if ( !s.contains("RosterGroup") 498 && !s.toLowerCase().startsWith("sys") 499 && !s.toUpperCase().startsWith("VSD")) { // NOI18N 500 result.add(s); 501 } 502 } 503 attributeKeys = result.toArray(String[]::new); 504 } 505 return attributeKeys; 506 } 507 508 private int getAttributeColumn(String attr) { 509 var names = getModelAttributeKeyColumnNames(); 510 for (int i = 0; i < names.length; i++) { 511 if (names[i].equals(attr)) { 512 return i + NUMCOL; 513 } 514 } 515 return 0; 516 } 517 518 private String getAttributeKey(int col) { 519 if ( col >= NUMCOL && col < getColumnCount() ) { 520 return getModelAttributeKeyColumnNames()[col - NUMCOL ]; 521 } 522 return ""; 523 } 524 525 // drop listeners 526 public void dispose() { 527 Roster.getDefault().removePropertyChangeListener(this); 528 Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach( re -> 529 re.removePropertyChangeListener(this) ); 530 } 531 532 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterTableModel.class); 533 534}