001package jmri.jmrit.roster.swing; 002 003import java.awt.event.ActionEvent; 004import java.awt.event.ActionListener; 005import java.beans.PropertyChangeEvent; 006import java.beans.PropertyChangeListener; 007import java.util.List; 008import javax.swing.JComboBox; 009import jmri.jmrit.roster.Roster; 010import jmri.jmrit.roster.RosterEntry; 011import jmri.jmrit.roster.RosterEntrySelector; 012import org.slf4j.Logger; 013import org.slf4j.LoggerFactory; 014 015/** 016 * A JComboBox containing roster entries or a string indicating that no roster 017 * entry is selected. 018 * <p> 019 * This is a JComboBox<Object> so that it can represent both. 020 * <p> 021 * This class has a self contained data model, and will automatically update the 022 * display if a RosterEntry is added, removed, or changes. 023 * 024 * @author Randall Wood Copyright (C) 2011 025 * @see jmri.jmrit.roster.Roster 026 * @see jmri.jmrit.roster.RosterEntry 027 * @see javax.swing.JComboBox 028 */ 029public class RosterEntryComboBox extends JComboBox<Object> implements RosterEntrySelector { 030 031 protected Roster _roster; 032 protected String _group; 033 protected String _roadName; 034 protected String _roadNumber; 035 protected String _dccAddress; 036 protected String _mfg; 037 protected String _decoderMfgID; 038 protected String _decoderVersionID; 039 protected String _id; 040 protected String _nonSelectedItem = Bundle.getMessage("RosterEntryComboBoxNoSelection"); 041 protected RosterEntry[] _currentSelection = null; 042 043 private final static Logger log = LoggerFactory.getLogger(RosterEntryComboBox.class); 044 045 /** 046 * Create a combo box with the default Roster and all entries in the active 047 * roster group. 048 */ 049 public RosterEntryComboBox() { 050 this(Roster.getDefault(), Roster.getDefault().getDefaultRosterGroup(), null, null, null, null, null, null, null); 051 } 052 053 /** 054 * Create a combo box with an arbitrary Roster and all entries in the active 055 * roster group. 056 * @param roster roster to use. 057 */ 058 public RosterEntryComboBox(Roster roster) { 059 this(roster, Roster.getDefault().getDefaultRosterGroup(), null, null, null, null, null, null, null); 060 } 061 062 /** 063 * Create a combo box with the default Roster and all entries in an 064 * arbitrary roster group. 065 * @param rosterGroup group to display. 066 */ 067 public RosterEntryComboBox(String rosterGroup) { 068 this(Roster.getDefault(), rosterGroup, null, null, null, null, null, null, null); 069 } 070 071 /** 072 * Create a combo box with an arbitrary Roster and all entries in an 073 * arbitrary roster group. 074 * @param roster roster to use. 075 * @param rosterGroup group to display. 076 */ 077 public RosterEntryComboBox(Roster roster, String rosterGroup) { 078 this(roster, rosterGroup, null, null, null, null, null, null, null); 079 } 080 081 /** 082 * Create a combo box with the default Roster and entries in the active 083 * roster group matching the specified attributes. Attributes with a null 084 * value will not be considered when filtering the roster entries. 085 * @param roadName road name. 086 * @param roadNumber road number. 087 * @param dccAddress dcc address. 088 * @param mfg manufacturer. 089 * @param decoderMfgID decoder manufacturer. 090 * @param decoderVersionID decoder version id. 091 * @param id roster id. * 092 */ 093 public RosterEntryComboBox(String roadName, 094 String roadNumber, 095 String dccAddress, 096 String mfg, 097 String decoderMfgID, 098 String decoderVersionID, 099 String id) { 100 this(Roster.getDefault(), 101 Roster.getDefault().getDefaultRosterGroup(), 102 roadName, 103 roadNumber, 104 dccAddress, 105 mfg, 106 decoderMfgID, 107 decoderVersionID, 108 id); 109 } 110 111 /** 112 * Create a combo box with an arbitrary Roster and entries in the active 113 * roster group matching the specified attributes. Attributes with a null 114 * value will not be considered when filtering the roster entries. 115 * 116 * @param roster roster to use. 117 * @param roadName road name. 118 * @param roadNumber road number. 119 * @param dccAddress dcc address. 120 * @param mfg manufacturer. 121 * @param decoderMfgID decoder manufacturer. 122 * @param decoderVersionID decoder version id. 123 * @param id roster id. 124 */ 125 public RosterEntryComboBox(Roster roster, 126 String roadName, 127 String roadNumber, 128 String dccAddress, 129 String mfg, 130 String decoderMfgID, 131 String decoderVersionID, 132 String id) { 133 this(roster, 134 Roster.getDefault().getDefaultRosterGroup(), 135 roadName, 136 roadNumber, 137 dccAddress, 138 mfg, 139 decoderMfgID, 140 decoderVersionID, 141 id); 142 143 } 144 145 /** 146 * Create a combo box with the default Roster and entries in an arbitrary 147 * roster group matching the specified attributes. Attributes with a null 148 * value will not be considered when filtering the roster entries. 149 * 150 * @param rosterGroup group to display. 151 * @param roadName road name. 152 * @param roadNumber road number. 153 * @param dccAddress dcc address. 154 * @param mfg manufacturer. 155 * @param decoderMfgID decoder manufacturer. 156 * @param decoderVersionID decoder version id. 157 * @param id roster id. 158 */ 159 public RosterEntryComboBox(String rosterGroup, 160 String roadName, 161 String roadNumber, 162 String dccAddress, 163 String mfg, 164 String decoderMfgID, 165 String decoderVersionID, 166 String id) { 167 this(Roster.getDefault(), 168 rosterGroup, 169 roadName, 170 roadNumber, 171 dccAddress, 172 mfg, 173 decoderMfgID, 174 decoderVersionID, 175 id); 176 } 177 178 /** 179 * Create a combo box with an arbitrary Roster and entries in an arbitrary 180 * roster group matching the specified attributes. Attributes with a null 181 * value will not be considered when filtering the roster entries. 182 * <p> 183 * All attributes used to filter roster entries are retained and reused when 184 * updating the combo box unless new attributes are specified when calling 185 * update. 186 * <p> 187 * All other constructors call this constructor with various default 188 * parameters. 189 * 190 * @param roster roster to use. 191 * @param rosterGroup group to display. 192 * @param roadName road name. 193 * @param roadNumber road number. 194 * @param dccAddress dcc address. 195 * @param mfg manufacturer. 196 * @param decoderMfgID decoder manufacturer. 197 * @param decoderVersionID decoder version id. 198 * @param id roster id. 199 */ 200 public RosterEntryComboBox(Roster roster, 201 String rosterGroup, 202 String roadName, 203 String roadNumber, 204 String dccAddress, 205 String mfg, 206 String decoderMfgID, 207 String decoderVersionID, 208 String id) { 209 super(); 210 setRenderer(new jmri.jmrit.roster.swing.RosterEntryListCellRenderer()); 211 _roster = roster; 212 _group = rosterGroup; 213 update(rosterGroup, 214 roadName, 215 roadNumber, 216 dccAddress, 217 mfg, 218 decoderMfgID, 219 decoderVersionID, 220 id); 221 222 _roster.addPropertyChangeListener(new PropertyChangeListener() { 223 @Override 224 public void propertyChange(PropertyChangeEvent pce) { 225 if (pce.getPropertyName().equals("add") 226 || pce.getPropertyName().equals("remove") 227 || pce.getPropertyName().equals("change")) { 228 update(); 229 } 230 } 231 }); 232 233 this.addActionListener(new ActionListener() { 234 235 @Override 236 public void actionPerformed(ActionEvent ae) { 237 fireSelectedRosterEntriesPropertyChange(); 238 } 239 }); 240 241 _nonSelectedItem = Bundle.getMessage("RosterEntryComboBoxNoSelection"); 242 } 243 244 /** 245 * Update the combo box with the currently selected roster group, using the 246 * same roster entry attributes specified in a prior call to update or when 247 * creating the combo box. 248 */ 249 public void update() { 250 update(this._group, 251 _roadName, 252 _roadNumber, 253 _dccAddress, 254 _mfg, 255 _decoderMfgID, 256 _decoderVersionID, 257 _id); 258 } 259 260 /** 261 * Update the combo box with an arbitrary roster group, using the same 262 * roster entry attributes specified in a prior call to update or when 263 * creating the combo box. 264 * @param rosterGroup group to display. 265 */ 266 public final void update(String rosterGroup) { 267 update(rosterGroup, 268 _roadName, 269 _roadNumber, 270 _dccAddress, 271 _mfg, 272 _decoderMfgID, 273 _decoderVersionID, 274 _id); 275 } 276 277 /** 278 * Update the combo box with the currently selected roster group, using new 279 * roster entry attributes. 280 * @param roadName road name. 281 * @param roadNumber road number. 282 * @param dccAddress dcc address. 283 * @param mfg manufacturer. 284 * @param decoderMfgID decoder manufacturer. 285 * @param decoderVersionID decoder version id. 286 * @param id roster id. 287 */ 288 public void update(String roadName, 289 String roadNumber, 290 String dccAddress, 291 String mfg, 292 String decoderMfgID, 293 String decoderVersionID, 294 String id) { 295 update(this._group, 296 roadName, 297 roadNumber, 298 dccAddress, 299 mfg, 300 decoderMfgID, 301 decoderVersionID, 302 id); 303 } 304 305 /** 306 * Update the combo box with an arbitrary roster group, using new roster 307 * entry attributes. 308 * @param rosterGroup group to display. 309 * @param roadName road name. 310 * @param roadNumber road number. 311 * @param dccAddress dcc address. 312 * @param mfg manufacturer. 313 * @param decoderMfgID decoder manufacturer. 314 * @param decoderVersionID decoder version id. 315 * @param id roster id. 316 */ 317 public final void update(String rosterGroup, 318 String roadName, 319 String roadNumber, 320 String dccAddress, 321 String mfg, 322 String decoderMfgID, 323 String decoderVersionID, 324 String id) { 325 Object selection = this.getSelectedItem(); 326 if (log.isDebugEnabled()) { 327 log.debug("Old selection: {}", selection); 328 log.debug("Old group: {}", _group); 329 } 330 ActionListener[] ALs = this.getActionListeners(); 331 for (ActionListener al : ALs) { 332 this.removeActionListener(al); 333 } 334 this.setSelectedItem(null); 335 List<RosterEntry> l = _roster.matchingList(roadName, 336 roadNumber, 337 dccAddress, 338 mfg, 339 decoderMfgID, 340 decoderVersionID, 341 id); 342 _group = rosterGroup; 343 _roadName = roadName; 344 _roadNumber = roadNumber; 345 _dccAddress = dccAddress; 346 _mfg = mfg; 347 _decoderMfgID = decoderMfgID; 348 _decoderVersionID = decoderVersionID; 349 _id = id; 350 removeAllItems(); 351 if (_nonSelectedItem != null) { 352 insertItemAt(_nonSelectedItem, 0); 353 setSelectedItem(_nonSelectedItem); 354 } 355 for (RosterEntry r : l) { 356 if (rosterGroup != null && !rosterGroup.equals(Roster.ALLENTRIES)) { 357 if (r.getAttribute(Roster.getRosterGroupProperty(rosterGroup)) != null 358 && r.getAttribute(Roster.getRosterGroupProperty(rosterGroup)).equals("yes")) { 359 addItem(r); 360 } 361 } else { 362 addItem(r); 363 } 364 if (r.equals(selection)) { 365 this.setSelectedItem(r); 366 } 367 } 368 if (log.isDebugEnabled()) { 369 log.debug("New selection: {}", this.getSelectedItem()); 370 log.debug("New group: {}", _group); 371 } 372 for (ActionListener al : ALs) { 373 this.addActionListener(al); 374 } 375 // fire the action event only if selection is not in the updated combobox 376 // don't use equals() since selection or getSelectedItem could be null 377 if (this.getSelectedItem() != selection) { 378 this.fireActionEvent(); 379 // this is part of the RosterEntrySelector contract 380 this.fireSelectedRosterEntriesPropertyChange(); 381 } 382 } 383 384 /** 385 * Set the text of the item that visually indicates that no roster entry is 386 * selected in the comboBox. 387 * @param itemText text to indicate no entry. 388 */ 389 public void setNonSelectedItem(String itemText) { 390 _nonSelectedItem = itemText; 391 update(_group); 392 } 393 394 /** 395 * Get the text of the item that visually indicates that no roster entry is 396 * selected in the comboBox. 397 * 398 * If this returns null, it indicates that the comboBox has no special item 399 * to indicate an empty selection. 400 * 401 * @return The text or null 402 */ 403 public String getNonSelectedItem() { 404 return _nonSelectedItem; 405 } 406 407 @Override 408 public RosterEntry[] getSelectedRosterEntries() { 409 return getSelectedRosterEntries(false); 410 } 411 412 // internally, we sometimes want to be able to force the reconstruction of 413 // the cached value returned by getSelectedRosterEntries 414 protected RosterEntry[] getSelectedRosterEntries(boolean force) { 415 if (_currentSelection == null || force) { 416 if (this.getSelectedItem() != null && !this.getSelectedItem().equals(_nonSelectedItem)) { 417 _currentSelection = new RosterEntry[1]; 418 _currentSelection[0] = (RosterEntry) this.getSelectedItem(); 419 } else { 420 _currentSelection = new RosterEntry[0]; 421 } 422 } 423 return _currentSelection; 424 } 425 426 // this method allows anonymous listeners to fire the "selectedRosterEntries" property change 427 protected void fireSelectedRosterEntriesPropertyChange() { 428 this.firePropertyChange(RosterEntrySelector.SELECTED_ROSTER_ENTRIES, 429 _currentSelection, 430 this.getSelectedRosterEntries(true)); 431 } 432 433}