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 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025/** 026 * Table data model for display of Roster variable values. 027 * <p> 028 * Any desired ordering, etc, is handled outside this class. 029 * <p> 030 * The initial implementation doesn't automatically update when roster entries 031 * change, doesn't allow updating of the entries, and only shows some of the 032 * fields. But it's a start.... 033 * 034 * @author Bob Jacobsen Copyright (C) 2009, 2010 035 * @since 2.7.5 036 */ 037public class RosterTableModel extends DefaultTableModel implements PropertyChangeListener { 038 039 public static final int IDCOL = 0; 040 static final int ADDRESSCOL = 1; 041 static final int ICONCOL = 2; 042 static final int DECODERCOL = 3; 043 static final int ROADNAMECOL = 4; 044 static final int ROADNUMBERCOL = 5; 045 static final int MFGCOL = 6; 046 static final int MODELCOL = 7; 047 static final int OWNERCOL = 8; 048 static final int DATEUPDATECOL = 9; 049 public static final int PROTOCOL = 10; 050 public static final int NUMCOL = PROTOCOL + 1; 051 private String rosterGroup = null; 052 boolean editable = false; 053 054 public RosterTableModel() { 055 this(false); 056 } 057 058 public RosterTableModel(boolean editable) { 059 this.editable = editable; 060 Roster.getDefault().addPropertyChangeListener(RosterTableModel.this); 061 setRosterGroup(null); // add prop change listeners to roster entries 062 } 063 064 /** 065 * Create a table model for a Roster group. 066 * 067 * @param group the roster group to show; if null, behaves the same as 068 * {@link #RosterTableModel()} 069 */ 070 public RosterTableModel(@CheckForNull RosterGroup group) { 071 this(false); 072 if (group != null) { 073 this.setRosterGroup(group.getName()); 074 } 075 } 076 077 @Override 078 public void propertyChange(PropertyChangeEvent e) { 079 if (e.getPropertyName().equals(Roster.ADD)) { 080 setRosterGroup(getRosterGroup()); // add prop change listener to new entry 081 fireTableDataChanged(); 082 } else if (e.getPropertyName().equals(Roster.REMOVE)) { 083 fireTableDataChanged(); 084 } else if (e.getPropertyName().equals(Roster.SAVED)) { 085 //TODO This really needs to do something like find the index of the roster entry here 086 if (e.getSource() instanceof RosterEntry) { 087 int row = Roster.getDefault().getGroupIndex(rosterGroup, (RosterEntry) e.getSource()); 088 fireTableRowsUpdated(row, row); 089 } else { 090 fireTableDataChanged(); 091 } 092 } else if (e.getPropertyName().equals(RosterGroupSelector.SELECTED_ROSTER_GROUP)) { 093 setRosterGroup((e.getNewValue() != null) ? e.getNewValue().toString() : null); 094 } else if (e.getPropertyName().startsWith("attribute") && e.getSource() instanceof RosterEntry) { // NOI18N 095 int row = Roster.getDefault().getGroupIndex(rosterGroup, (RosterEntry) e.getSource()); 096 fireTableRowsUpdated(row, row); 097 } else if (e.getPropertyName().equals(Roster.ROSTER_GROUP_ADDED) && e.getNewValue().equals(rosterGroup)) { 098 fireTableDataChanged(); 099 } 100 } 101 102 @Override 103 public int getRowCount() { 104 return Roster.getDefault().numGroupEntries(rosterGroup); 105 } 106 107 @Override 108 public int getColumnCount() { 109 return NUMCOL + getModelAttributeKeyColumnNames().length; 110 } 111 112 @Override 113 public String getColumnName(int col) { 114 switch (col) { 115 case IDCOL: 116 return Bundle.getMessage("FieldID"); 117 case ADDRESSCOL: 118 return Bundle.getMessage("FieldDCCAddress"); 119 case DECODERCOL: 120 return Bundle.getMessage("FieldDecoderModel"); 121 case MODELCOL: 122 return Bundle.getMessage("FieldModel"); 123 case ROADNAMECOL: 124 return Bundle.getMessage("FieldRoadName"); 125 case ROADNUMBERCOL: 126 return Bundle.getMessage("FieldRoadNumber"); 127 case MFGCOL: 128 return Bundle.getMessage("FieldManufacturer"); 129 case ICONCOL: 130 return Bundle.getMessage("FieldIcon"); 131 case OWNERCOL: 132 return Bundle.getMessage("FieldOwner"); 133 case DATEUPDATECOL: 134 return Bundle.getMessage("FieldDateUpdated"); 135 case PROTOCOL: 136 return Bundle.getMessage("FieldProtocol"); 137 default: 138 return getColumnNameAttribute(col); 139 } 140 } 141 142 private String getColumnNameAttribute(int col) { 143 if ( col < getColumnCount() ) { 144 String attributeKey = getAttributeKey(col); 145 try { 146 return Bundle.getMessage(attributeKey); 147 } catch (java.util.MissingResourceException ex){} 148 149 String[] r = attributeKey.split("(?=\\p{Lu})"); // NOI18N 150 StringBuilder sb = new StringBuilder(); 151 sb.append(r[0].trim()); 152 for (int j = 1; j < r.length; j++) { 153 sb.append(" "); 154 sb.append(r[j].trim()); 155 } 156 return sb.toString(); 157 } 158 return "<UNKNOWN>"; // NOI18N 159 } 160 161 @Override 162 public Class<?> getColumnClass(int col) { 163 switch (col) { 164 case ADDRESSCOL: 165 return Integer.class; 166 case ICONCOL: 167 return ImageIcon.class; 168 case DATEUPDATECOL: 169 return Date.class; 170 default: 171 return getColumnClassAttribute(col); 172 } 173 } 174 175 private Class<?> getColumnClassAttribute(int col){ 176 if (RosterEntry.ATTRIBUTE_LAST_OPERATED.equals( getAttributeKey(col))) { 177 return Date.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 return (value == null ? "" : value); 285 } 286 287 @Override 288 public void setValueAt(Object value, int row, int col) { 289 // get roster entry for row 290 RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row); 291 if (re == null) { 292 log.warn("roster entry is null!"); 293 return; 294 } 295 if (re.isOpen()) { 296 log.warn("Entry is already open"); 297 return; 298 } 299 if (Objects.equals(value, getValueAt(row, col))) { 300 return; 301 } 302 String valueToSet = (String) value; 303 switch (col) { 304 case IDCOL: 305 re.setId(valueToSet); 306 break; 307 case ROADNAMECOL: 308 re.setRoadName(valueToSet); 309 break; 310 case ROADNUMBERCOL: 311 re.setRoadNumber(valueToSet); 312 break; 313 case MFGCOL: 314 re.setMfg(valueToSet); 315 break; 316 case MODELCOL: 317 re.setModel(valueToSet); 318 break; 319 case OWNERCOL: 320 re.setOwner(valueToSet); 321 break; 322 default: 323 setValueAtAttribute(valueToSet, re, col); 324 break; 325 } 326 // need to mark as updated 327 re.changeDateUpdated(); 328 re.updateFile(); 329 } 330 331 private void setValueAtAttribute(String valueToSet, RosterEntry re, int col) { 332 String attributeKey = getAttributeKey(col); 333 if ((valueToSet == null) || valueToSet.isEmpty()) { 334 re.deleteAttribute(attributeKey); 335 } else { 336 re.putAttribute(attributeKey, valueToSet); 337 } 338 } 339 340 public int getPreferredWidth(int column) { 341 int retval = 20; // always take some width 342 retval = Math.max(retval, new JLabel(getColumnName(column)).getPreferredSize().width + 15); // leave room for sorter arrow 343 for (int row = 0; row < getRowCount(); row++) { 344 if (getColumnClass(column).equals(String.class)) { 345 retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width); 346 } else if (getColumnClass(column).equals(Integer.class)) { 347 retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width); 348 } else if (getColumnClass(column).equals(ImageIcon.class)) { 349 retval = Math.max(retval, new JLabel((Icon) getValueAt(row, column)).getPreferredSize().width); 350 } 351 } 352 return retval + 5; 353 } 354 355 public final void setRosterGroup(String rosterGroup) { 356 Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach((re) -> { 357 re.removePropertyChangeListener(this); 358 }); 359 this.rosterGroup = rosterGroup; 360 Roster.getDefault().getEntriesInGroup(rosterGroup).forEach((re) -> { 361 re.addPropertyChangeListener(this); 362 }); 363 fireTableDataChanged(); 364 } 365 366 public final String getRosterGroup() { 367 return this.rosterGroup; 368 } 369 370 // access via method to ensure not null 371 private String[] attributeKeys = null; 372 373 private String[] getModelAttributeKeyColumnNames() { 374 if ( attributeKeys == null ) { 375 Set<String> result = new TreeSet<>(); 376 for (String s : Roster.getDefault().getAllAttributeKeys()) { 377 if ( !s.contains("RosterGroup") 378 && !s.toLowerCase().startsWith("sys") 379 && !s.toUpperCase().startsWith("VSD")) { // NOI18N 380 result.add(s); 381 } 382 } 383 attributeKeys = result.toArray(String[]::new); 384 } 385 return attributeKeys; 386 } 387 388 private String getAttributeKey(int col) { 389 if ( col >= NUMCOL && col < getColumnCount() ) { 390 return getModelAttributeKeyColumnNames()[col - NUMCOL ]; 391 } 392 return ""; 393 } 394 395 // drop listeners 396 public void dispose() { 397 Roster.getDefault().removePropertyChangeListener(this); 398 Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach((re) -> { 399 re.removePropertyChangeListener(this); 400 }); 401 } 402 403 private final static Logger log = LoggerFactory.getLogger(RosterTableModel.class); 404}