001package jmri.jmrit;
002
003import java.awt.Font;
004import java.awt.event.ComponentAdapter;
005import java.awt.event.ComponentEvent;
006import java.awt.event.KeyListener;
007import java.util.ResourceBundle;
008import javax.swing.BoxLayout;
009import javax.swing.JComboBox;
010import javax.swing.JPanel;
011import javax.swing.JTextField;
012import jmri.DccLocoAddress;
013import jmri.InstanceManager;
014import jmri.LocoAddress;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Tool for selecting short/long address for DCC throttles.
020 *
021 * This is made more complex because we want it to appear easier. Some DCC
022 * systems allow addresses like 112 to be either long (extended) or short;
023 * others default to one or the other.
024 * <p>
025 * When locked (the default), the short/long selection is forced to stay in
026 * synch with what's available from the current ThrottleManager. If unlocked,
027 * this can differ if it's been explicity specified via the GUI (e.g. you can
028 * call 63 a long address even if the DCC system can't actually do it right
029 * now). This is useful in decoder programming, for example, where you might be
030 * configuring a loco to run somewhere else.
031 *
032 * @author Bob Jacobsen Copyright (C) 2005
033 */
034public class DccLocoAddressSelector extends JPanel {
035
036    JComboBox<String> box = null;
037    JTextField text = new JTextField();
038
039    private static final int FONT_SIZE_MIN = 12;
040    private static final int FONT_SIZE_MAX = 96;
041    private static final int FONT_INCREMENT = 2;
042
043    public DccLocoAddressSelector() {
044        super();
045        if ((InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null)
046                && !InstanceManager.throttleManagerInstance().addressTypeUnique()) {
047            configureBox(InstanceManager.throttleManagerInstance().getAddressTypes());
048        } else {
049            configureBox(
050                    new String[]{LocoAddress.Protocol.DCC_SHORT.getPeopleName(),
051                        LocoAddress.Protocol.DCC_LONG.getPeopleName()});
052        }
053    }
054
055    public DccLocoAddressSelector(String[] protocols) {
056        super();
057        configureBox(protocols);
058    }
059
060    private void configureBox(String[] protocols) {
061        box = new JComboBox<>(protocols);
062        box.setSelectedIndex(0);
063        text = new JTextField();
064        text.setColumns(4);
065        text.setToolTipText(rb.getString("TooltipTextFieldEnabled"));
066        box.setToolTipText(rb.getString("TooltipComboBoxEnabled"));
067
068    }
069
070    public void setLocked(boolean l) {
071        locked = l;
072    }
073
074    public boolean getLocked(boolean l) {
075        return locked;
076    }
077    private boolean locked = true;
078
079    private boolean boxUsed = false;
080    private boolean textUsed = false;
081    private boolean panelUsed = false;
082
083    /**
084     * Get the currently selected DCC address.
085     * <p>
086     * This is the primary output of this class.
087     * @return DccLocoAddress object containing GUI choices, or null if no entries in GUI
088     */
089    public DccLocoAddress getAddress() {
090        // no object if no address
091        if (text.getText().isEmpty()) {
092            return null;
093        }
094
095        // ask the Throttle Manager to handle this!
096        LocoAddress.Protocol protocol;
097        if (InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null) {
098            protocol = InstanceManager.throttleManagerInstance().getProtocolFromString((String) box.getSelectedItem());
099            return (DccLocoAddress) InstanceManager.throttleManagerInstance().getAddress(text.getText(), protocol);
100        }
101
102        // nothing, construct a default
103        int num = Integer.parseInt(text.getText());
104        protocol = LocoAddress.Protocol.getByPeopleName((String) box.getSelectedItem());
105        return new DccLocoAddress(num, protocol);
106    }
107
108    public void setAddress(DccLocoAddress a) {
109        if (a != null) {
110            if (a instanceof jmri.jmrix.openlcb.OpenLcbLocoAddress) {
111                // now special case, should be refactored
112                jmri.jmrix.openlcb.OpenLcbLocoAddress oa = (jmri.jmrix.openlcb.OpenLcbLocoAddress) a;
113                text.setText(oa.getNode().toString());
114                if (!followingAnotherSelector) {
115                    box.setSelectedItem(jmri.LocoAddress.Protocol.OPENLCB.getPeopleName());
116                }
117            } else {
118                text.setText("" + a.getNumber());
119                if (InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null) {
120                    if (!followingAnotherSelector) {
121                        box.setSelectedItem(InstanceManager.throttleManagerInstance().getAddressTypeString(a.getProtocol()));
122                    }
123                } else {
124                    if (!followingAnotherSelector) {
125                        box.setSelectedItem(a.getProtocol().getPeopleName());
126                    }
127                }
128            }
129        }
130    }
131
132    public void setVariableSize(boolean s) {
133        varFontSize = s;
134    }
135    boolean varFontSize = false;
136
137    /**
138     * Put back to original state, clearing GUI
139     */
140    public void reset() {
141        if (!followingAnotherSelector) {
142            box.setSelectedIndex(0);
143        }
144        text.setText("");
145    }
146
147    /** Get a JPanel containing the combined selector.
148     * <p>
149     * Because Swing only allows a component to be inserted in one
150     * container, this can only be done once
151     */
152    public JPanel getCombinedJPanel() {
153        if (panelUsed) {
154            log.error("getCombinedPanel invoked after panel already requested");
155            return null;
156        }
157        if (textUsed) {
158            log.error("getCombinedPanel invoked after text already requested");
159            return null;
160        }
161        if (boxUsed) {
162            log.error("getCombinedPanel invoked after text already requested");
163            return null;
164        }
165        panelUsed = true;
166
167        if (varFontSize) {
168            text.setFont(new Font("", Font.PLAIN, 32));
169        }
170
171        JPanel p = new JPanel();
172        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
173        p.add(text);
174        if (!locked
175                || ((InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null)
176                && !InstanceManager.throttleManagerInstance().addressTypeUnique())) {
177            p.add(box);
178        }
179
180        p.addComponentListener(
181                new ComponentAdapter() {
182                    @Override
183                    public void componentResized(ComponentEvent e) {
184                        changeFontSizes();
185                    }
186                });
187
188        return p;
189    }
190
191    /**
192     * The longest 4 character string. Used for resizing.
193     */
194    private static final String LONGEST_STRING = "MMMM";
195
196    /**
197     * A resizing has occurred, so determine the optimum font size for the
198     * localAddressField.
199     */
200    private void changeFontSizes() {
201        if (!varFontSize) {
202            return;
203        }
204        int fieldWidth = text.getSize().width;
205        int stringWidth = text.getFontMetrics(text.getFont()).stringWidth(LONGEST_STRING) + 8;
206        int fontSize = text.getFont().getSize();
207        if (stringWidth > fieldWidth) { // component has shrunk horizontally
208            while ((stringWidth > fieldWidth) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) {
209                fontSize -= FONT_INCREMENT;
210                Font f = new Font("", Font.PLAIN, fontSize);
211                text.setFont(f);
212                stringWidth = text.getFontMetrics(text.getFont()).stringWidth(LONGEST_STRING) + 8;
213            }
214        } else { // component has grown horizontally
215            while ((fieldWidth - stringWidth > 10) && (fontSize <= FONT_SIZE_MAX - FONT_INCREMENT)) {
216                fontSize += FONT_INCREMENT;
217                Font f = new Font("", Font.PLAIN, fontSize);
218                text.setFont(f);
219                stringWidth = text.getFontMetrics(text.getFont()).stringWidth(LONGEST_STRING) + 8;
220            }
221        }
222        // also fit vertically
223        int fieldHeight = text.getSize().height;
224        int stringHeight = text.getFontMetrics(text.getFont()).getHeight();
225        while ((stringHeight > fieldHeight) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) {  // component has shrunk vertically
226            fontSize -= FONT_INCREMENT;
227            Font f = new Font("", Font.PLAIN, fontSize);
228            text.setFont(f);
229            stringHeight = text.getFontMetrics(text.getFont()).getHeight();
230        }        
231    }
232
233    /**
234     * Provide a common setEnable call for the GUI components in the
235     * selector
236     */
237    @Override
238    public void setEnabled(boolean e) {
239        text.setEditable(e);
240        text.setEnabled(e);
241        text.setFocusable(e); // to not conflict with the throttle keyboad controls
242        if (e) {
243            text.setToolTipText(rb.getString("TooltipTextFieldEnabled"));
244        } else {
245            text.setToolTipText(rb.getString("TooltipTextFieldDisabled"));
246        }
247
248        // only change selection box state if not following
249        if (!followingAnotherSelector) {
250            box.setEnabled(e);
251            if (e) {
252                box.setToolTipText(rb.getString("TooltipComboBoxEnabled"));
253            } else {
254                box.setToolTipText(rb.getString("TooltipComboBoxDisabled"));
255            }
256        }
257    }
258
259    public void setEnabledProtocol(boolean e) {
260        box.setEnabled(e);
261        if (e) {
262            box.setToolTipText(rb.getString("TooltipComboBoxEnabled"));
263        } else {
264            box.setToolTipText(rb.getString("TooltipComboBoxDisabled"));
265        }
266    }
267
268    /**
269     * Get the text field for entering the number as a separate
270     * component.  
271     * <p>
272     * Because Swing only allows a component to be inserted in one
273     * container, this can only be done once
274     */
275    public JTextField getTextField() {
276        if (textUsed) {
277            reportError("getTextField invoked after text already requested");
278            return null;
279        }
280        textUsed = true;
281        return text;
282    }
283
284    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
285        justification="Error String needs to be evaluated unchanged.")
286    void reportError(String msg) {
287        log.error(msg, new Exception("traceback"));
288    }
289
290    /**
291     * Get the selector box for picking long/short as a separate
292     * component.
293     * Because Swing only allows a component to be inserted in one
294     * container, this can only be done once
295     */
296    public JComboBox<String> getSelector() {
297        if (boxUsed) {
298            log.error("getSelector invoked after text already requested");
299            return null;
300        }
301        boxUsed = true;
302        return box;
303    }
304
305    boolean followingAnotherSelector = false;
306    
307    /**
308     * This Selector's protocol box will follow another DccLocoAddressSelector's
309     * selected protocol, so this one's protocol choice is constrained to match.
310     * At present, this can't be undone.
311     * Meant for use in e.g. the consist tool for protocols that require only
312     * one type of protocol in a consist
313     */
314     public void followAnotherSelector(DccLocoAddressSelector selector) {
315        // add a listener to the other selector
316        selector.box.addItemListener(event -> {
317            var selection = selector.box.getSelectedItem();
318            this.box.setSelectedItem(selection);
319            this.box.setEnabled(false);
320        });
321        var selection = selector.box.getSelectedItem();
322        this.box.setSelectedItem(selection);
323        this.box.setEnabled(false);
324        box.setToolTipText(rb.getString("TooltipComboBoxDisabled"));
325        followingAnotherSelector = true;
326        
327     }
328    /*
329     * Override the addKeyListener method in JPanel so that we can set the
330     * text box as the object listening for keystrokes
331     */
332    @Override
333    public void addKeyListener(KeyListener l){
334       super.addKeyListener(l);
335       text.addKeyListener(l);
336    }
337
338    final static ResourceBundle rb = ResourceBundle.getBundle("jmri.jmrit.DccLocoAddressSelectorBundle");
339
340    private final static Logger log = LoggerFactory.getLogger(DccLocoAddressSelector.class);
341}