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}