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