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}