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