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