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                box.setSelectedItem(jmri.LocoAddress.Protocol.OPENLCB.getPeopleName());
115            } else {
116                text.setText("" + a.getNumber());
117                if (InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null) {
118                    box.setSelectedItem(InstanceManager.throttleManagerInstance().getAddressTypeString(a.getProtocol()));
119                } else {
120                    box.setSelectedItem(a.getProtocol().getPeopleName());
121                }
122            }
123        }
124    }
125
126    public void setVariableSize(boolean s) {
127        varFontSize = s;
128    }
129    boolean varFontSize = false;
130
131    /*
132     * Put back to original state, clearing GUI
133     */
134    public void reset() {
135        box.setSelectedIndex(0);
136        text.setText("");
137    }
138
139    /* Get a JPanel containing the combined selector.
140     * <p>
141     * Because Swing only allows a component to be inserted in one
142     * container, this can only be done once
143     */
144    public JPanel getCombinedJPanel() {
145        if (panelUsed) {
146            log.error("getCombinedPanel invoked after panel already requested");
147            return null;
148        }
149        if (textUsed) {
150            log.error("getCombinedPanel invoked after text already requested");
151            return null;
152        }
153        if (boxUsed) {
154            log.error("getCombinedPanel invoked after text already requested");
155            return null;
156        }
157        panelUsed = true;
158
159        if (varFontSize) {
160            text.setFont(new Font("", Font.PLAIN, 32));
161        }
162
163        JPanel p = new JPanel();
164        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
165        p.add(text);
166        if (!locked
167                || ((InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null)
168                && !InstanceManager.throttleManagerInstance().addressTypeUnique())) {
169            p.add(box);
170        }
171
172        p.addComponentListener(
173                new ComponentAdapter() {
174                    @Override
175                    public void componentResized(ComponentEvent e) {
176                        changeFontSizes();
177                    }
178                });
179
180        return p;
181    }
182
183    /**
184     * The longest 4 character string. Used for resizing.
185     */
186    private static final String LONGEST_STRING = "MMMM";
187
188    /**
189     * A resizing has occurred, so determine the optimum font size for the
190     * localAddressField.
191     */
192    private void changeFontSizes() {
193        if (!varFontSize) {
194            return;
195        }
196        int fieldWidth = text.getSize().width;
197        int stringWidth = text.getFontMetrics(text.getFont()).stringWidth(LONGEST_STRING) + 8;
198        int fontSize = text.getFont().getSize();
199        if (stringWidth > fieldWidth) { // component has shrunk horizontally
200            while ((stringWidth > fieldWidth) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) {
201                fontSize -= FONT_INCREMENT;
202                Font f = new Font("", Font.PLAIN, fontSize);
203                text.setFont(f);
204                stringWidth = text.getFontMetrics(text.getFont()).stringWidth(LONGEST_STRING) + 8;
205            }
206        } else { // component has grown horizontally
207            while ((fieldWidth - stringWidth > 10) && (fontSize <= FONT_SIZE_MAX - FONT_INCREMENT)) {
208                fontSize += FONT_INCREMENT;
209                Font f = new Font("", Font.PLAIN, fontSize);
210                text.setFont(f);
211                stringWidth = text.getFontMetrics(text.getFont()).stringWidth(LONGEST_STRING) + 8;
212            }
213        }
214        // also fit vertically
215        int fieldHeight = text.getSize().height;
216        int stringHeight = text.getFontMetrics(text.getFont()).getHeight();
217        while ((stringHeight > fieldHeight) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) {  // component has shrunk vertically
218            fontSize -= FONT_INCREMENT;
219            Font f = new Font("", Font.PLAIN, fontSize);
220            text.setFont(f);
221            stringHeight = text.getFontMetrics(text.getFont()).getHeight();
222        }        
223    }
224
225    /*
226     * Provide a common setEnable call for the GUI components in the
227     * selector
228     */
229    @Override
230    public void setEnabled(boolean e) {
231        text.setEditable(e);
232        text.setEnabled(e);
233        text.setFocusable(e); // to not conflict with the throttle keyboad controls
234        box.setEnabled(e);
235        if (e) {
236            text.setToolTipText(rb.getString("TooltipTextFieldEnabled"));
237            box.setToolTipText(rb.getString("TooltipComboBoxEnabled"));
238        } else {
239            text.setToolTipText(rb.getString("TooltipTextFieldDisabled"));
240            box.setToolTipText(rb.getString("TooltipComboBoxDisabled"));
241        }
242    }
243
244    public void setEnabledProtocol(boolean e) {
245        box.setEnabled(e);
246        if (e) {
247            box.setToolTipText(rb.getString("TooltipComboBoxEnabled"));
248        } else {
249            box.setToolTipText(rb.getString("TooltipComboBoxDisabled"));
250        }
251    }
252
253    /*
254     * Get the text field for entering the number as a separate
255     * component.  
256     * <p>
257     * Because Swing only allows a component to be inserted in one
258     * container, this can only be done once
259     */
260    public JTextField getTextField() {
261        if (textUsed) {
262            reportError("getTextField invoked after text already requested");
263            return null;
264        }
265        textUsed = true;
266        return text;
267    }
268
269    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
270        justification="Error String needs to be evaluated unchanged.")
271    void reportError(String msg) {
272        log.error(msg, new Exception("traceback"));
273    }
274
275    /*
276     * Get the selector box for picking long/short as a separate
277     * component.
278     * Because Swing only allows a component to be inserted in one
279     * container, this can only be done once
280     */
281    public JComboBox<String> getSelector() {
282        if (boxUsed) {
283            log.error("getSelector invoked after text already requested");
284            return null;
285        }
286        boxUsed = true;
287        return box;
288    }
289
290    /*
291     * Override the addKeyListener method in JPanel so that we can set the
292     * text box as the object listening for keystrokes
293     */
294    @Override
295    public void addKeyListener(KeyListener l){
296       super.addKeyListener(l);
297       text.addKeyListener(l);
298    }
299
300    final static ResourceBundle rb = ResourceBundle.getBundle("jmri.jmrit.DccLocoAddressSelectorBundle");
301
302    private final static Logger log = LoggerFactory.getLogger(DccLocoAddressSelector.class);
303}