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&lt;Object&gt; 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}