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