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