001package jmri.jmrix; 002 003import java.awt.Color; 004import java.awt.event.ActionEvent; 005import java.awt.event.ActionListener; 006import java.io.File; 007import java.io.FileNotFoundException; 008import java.io.IOException; 009 010import javax.swing.Box; 011import javax.swing.BoxLayout; 012import javax.swing.ButtonGroup; 013import javax.swing.JButton; 014import javax.swing.JFileChooser; 015import javax.swing.JLabel; 016import javax.swing.JPanel; 017import javax.swing.JProgressBar; 018import javax.swing.JRadioButton; 019import javax.swing.JSeparator; 020import javax.swing.JTextField; 021import jmri.jmrit.MemoryContents; 022import jmri.util.FileUtil; 023import jmri.util.swing.JmriJOptionPane; 024import jmri.util.swing.WrapLayout; 025 026/** 027 * Pane for downloading .hex files and .dmf files to those LocoNet devices which 028 * support firmware updates via LocoNet IPL messages. 029 * 030 * This version relies on the file contents interpretation mechanisms built into 031 * the readHex() methods found in class jmri.jmrit.MemoryContents to 032 * automatically interpret the file's addressing type - either 16-bit or 24-bit 033 * addressing. The interpreted addressing type is reported in the pane after a 034 * file is read. The user cannot select the addressing type. 035 * 036 * This version relies on the file contents checking mechanisms built into the 037 * readHex() methods found in class jmri.jmrit.MemoryContents to check for a 038 * wide variety of possible issues in the contents of the firmware update file. 039 * Any exception thrown by at method is used to select an error message to 040 * display in the status line of the pane. 041 * 042 * @author Bob Jacobsen Copyright (C) 2005, 2015 043 * @author B. Milhaupt Copyright (C) 2013, 2014, 2017 044 */ 045public abstract class AbstractLoaderPane extends jmri.util.swing.JmriPanel 046 implements ActionListener { 047 048 // GUI member declarations 049 JLabel inputFileName = new JLabel(""); 050 051 protected JButton selectButton; 052 protected JButton loadButton; 053 protected JButton verifyButton; // protected so subclass can set invisible 054 protected JButton abortButton; 055 056 JRadioButton address24bit = new JRadioButton(Bundle.getMessage("Button24bit")); 057 JRadioButton address16bit = new JRadioButton(Bundle.getMessage("Button16bit")); 058 protected ButtonGroup addressSizeButtonGroup = new ButtonGroup(); 059 060 protected JProgressBar bar; 061 protected JLabel status = new JLabel(""); 062 JPanel inputFileNamePanel; 063 064 protected MemoryContents inputContent = new MemoryContents(); 065 private int inputFileLabelWidth; 066 067 public AbstractLoaderPane() { 068 } 069 070 /** 071 * {@inheritDoc} 072 */ 073 @Override 074 abstract public String getHelpTarget(); 075 076 /** 077 * Include code to add additional options here. By convention, if you 078 * include visible options, follow with a JSeparator. 079 */ 080 protected void addOptionsPanel() { 081 } 082 083 /** 084 * {@inheritDoc} 085 */ 086 @Override 087 public void initComponents() { 088 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 089 090 { 091 /* Create panels for displaying a filename and for providing a file 092 * selection pushbutton 093 */ 094 inputFileNamePanel = new JPanel(); 095 inputFileNamePanel.setLayout(new WrapLayout()); 096 JLabel l = new JLabel(Bundle.getMessage("LabelInpFile")); 097 inputFileLabelWidth = l.getMinimumSize().width; 098 l.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT); 099 inputFileNamePanel.add(l); 100 inputFileNamePanel.add(new Box.Filler(new java.awt.Dimension(5, 20), 101 new java.awt.Dimension(5, 20), 102 new java.awt.Dimension(5, 20))); 103 inputFileNamePanel.add(inputFileName); 104 105 add(inputFileNamePanel); 106 107 JPanel p = new JPanel(); 108 p.setLayout(new WrapLayout()); 109 selectButton = new JButton(Bundle.getMessage("ButtonSelect")); 110 selectButton.addActionListener((ActionEvent e) -> { 111 inputContent = new MemoryContents(); 112 setDefaultFieldValues(); 113 updateDownloadVerifyButtons(); 114 selectInputFile(); 115 doRead(chooser); 116 }); 117 p.add(selectButton); 118 119 add(p); 120 } 121 122 { 123 // Create a panel for displaying the addressing type, via radio buttons 124 JPanel p = new JPanel(); 125 p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS)); 126 JLabel l = new JLabel(Bundle.getMessage("LabelBitMode") + " "); 127 l.setEnabled(false); 128 p.add(l); 129 p.add(address16bit); 130 p.add(address24bit); 131 addressSizeButtonGroup.add(address16bit); 132 addressSizeButtonGroup.add(address24bit); 133 addressSizeButtonGroup.clearSelection(); 134 address16bit.setEnabled(false); 135 address24bit.setEnabled(false); 136 add(p); 137 } 138 139 setDefaultFieldValues(); 140 141 add(new JSeparator()); 142 143 addOptionsPanel(); 144 145 { 146 // create a panel for the upload, verify, and abort buttons 147 JPanel p = new JPanel(); 148 p.setLayout(new WrapLayout()); 149 150 loadButton = new JButton(Bundle.getMessage("ButtonDownload")); 151 loadButton.setEnabled(false); 152 loadButton.setToolTipText(Bundle.getMessage("TipLoadDisabled")); 153 p.add(loadButton); 154 loadButton.addActionListener((java.awt.event.ActionEvent e) -> { 155 doLoad(); 156 }); 157 158 verifyButton = new JButton(Bundle.getMessage("ButtonVerify")); 159 verifyButton.setEnabled(false); 160 verifyButton.setToolTipText(Bundle.getMessage("TipVerifyDisabled")); 161 p.add(verifyButton); 162 verifyButton.addActionListener((java.awt.event.ActionEvent e) -> { 163 doVerify(); 164 }); 165 166 add(p); 167 168 abortButton = new JButton(Bundle.getMessage("ButtonAbort")); 169 abortButton.setEnabled(false); 170 abortButton.setToolTipText(Bundle.getMessage("TipAbortDisabled")); 171 p.add(abortButton); 172 abortButton.addActionListener((java.awt.event.ActionEvent e) -> { 173 setOperationAborted(true); 174 }); 175 176 add(p); 177 178 add(new JSeparator()); 179 180 // create progress bar 181 bar = new JProgressBar(0, 100); 182 bar.setStringPainted(true); 183 add(bar); 184 185 add(new JSeparator()); 186 187 { 188 // create a panel for displaying a status message 189 p = new JPanel(); 190 p.setLayout(new WrapLayout()); 191 status.setText(Bundle.getMessage("StatusSelectFile")); 192 // layout 193 status.setAlignmentX(JLabel.LEFT_ALIGNMENT); 194 status.setFont(status.getFont().deriveFont(0.9f * inputFileName.getFont().getSize())); // a bit smaller 195 status.setForeground(Color.gray); 196 p.add(status); 197 add(p); 198 } 199 200 } 201 } 202 203 // static so that the selection will be retained from 204 // one open LoaderPane to the next 205 private static JFileChooser chooser; 206 207 /** 208 * Add filter(s) for possible types to the input file chooser. 209 * 210 * @param chooser the file chooser to add filter(s) to 211 */ 212 protected void addChooserFilters(JFileChooser chooser) { 213 javax.swing.filechooser.FileNameExtensionFilter filter; 214 chooser.addChoosableFileFilter( 215 filter = new javax.swing.filechooser.FileNameExtensionFilter( 216 "Intel Hex Format Firmware (*.hex)", "hex")); // NOI18N 217 218 // make the downloadable file filter the default active filter 219 chooser.setFileFilter(filter); 220 } 221 222 private void selectInputFile() { 223 String name = inputFileName.getText(); 224 if (name.equals("")) { 225 name = FileUtil.getUserFilesPath(); 226 } 227 if (chooser == null) { 228 chooser = new jmri.util.swing.JmriJFileChooser(name); 229 addChooserFilters(chooser); 230 } 231 inputFileName.setText(""); // clear out in case of failure 232 int retVal = chooser.showOpenDialog(this); 233 if (retVal != JFileChooser.APPROVE_OPTION) { 234 return; // give up if no file selected 235 } 236 String newFileName = chooser.getSelectedFile().getName(); 237 inputFileName.setText(newFileName); 238 // check to see if it fits on the screen 239 double currentStringWidth = inputFileName.getMinimumSize().width; 240 double allowedWidth; 241 inputFileName.setToolTipText(newFileName); 242 allowedWidth = inputFileNamePanel.getSize().width * 4 / 5 - inputFileLabelWidth; 243 if (currentStringWidth > allowedWidth) { 244 // Filename won't fit on the display. 245 // need to shorten the string. 246 double startPoint 247 = (inputFileName.getText().length() 248 * (1.0 - (allowedWidth / currentStringWidth))); 249 String displayableName = "..." // NOI18N 250 + inputFileName.getText().substring((int) startPoint); 251 log.info("Shortening display of filename {} to {}", inputFileName.getText(), displayableName); // NOI18N 252 log.debug("Width required to display the full file name = {}", currentStringWidth); 253 log.debug("Allowed width = {}", allowedWidth); // NOI18N 254 log.debug("Amount of text not displayed = {}", startPoint); // NOI18N 255 inputFileName.setText(displayableName); 256 } 257 inputFileName.updateUI(); 258 inputFileNamePanel.updateUI(); 259 updateUI(); 260 261 loadButton.setEnabled(false); 262 loadButton.setToolTipText(Bundle.getMessage("TipLoadDisabled")); 263 verifyButton.setEnabled(false); 264 verifyButton.setToolTipText(Bundle.getMessage("TipVerifyDisabled")); 265 status.setText(Bundle.getMessage("StatusDoDownload")); 266 } 267 268 protected void handleOptionsInFileContent(MemoryContents inputContent) { 269 } 270 271 /** 272 * Read file into local memory. 273 * 274 * @param chooser chooser to select the file to read from 275 */ 276 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "SLF4J_FORMAT_SHOULD_BE_CONST", 277 justification = "Passing I18N exception text through to log") 278 protected void doRead(JFileChooser chooser) { 279 if (inputFileName.getText().equals("")) { 280 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorNoInputFile"), 281 Bundle.getMessage("ErrorTitle"), 282 JmriJOptionPane.ERROR_MESSAGE); 283 return; 284 } 285 286 // force load, verify disabled in case read fails 287 loadButton.setEnabled(false); 288 loadButton.setToolTipText(Bundle.getMessage("TipLoadDisabled")); 289 verifyButton.setEnabled(false); 290 verifyButton.setToolTipText(Bundle.getMessage("TipVerifyDisabled")); 291 abortButton.setEnabled(false); 292 abortButton.setToolTipText(Bundle.getMessage("TipAbortDisabled")); 293 294 // clear the existing memory contents 295 inputContent = new MemoryContents(); 296 297 bar.setValue(0); 298 299 // load 300 try { 301 inputContent.readHex(new File(chooser.getSelectedFile().getPath())); 302 } catch (FileNotFoundException f) { 303 log.error(f.getLocalizedMessage()); 304 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorFileNotFound"), 305 Bundle.getMessage("ErrorTitle"), 306 JmriJOptionPane.ERROR_MESSAGE); 307 status.setText(Bundle.getMessage("StatusFileNotFound")); 308 this.disableDownloadVerifyButtons(); 309 return; 310 } catch (MemoryContents.MemoryFileRecordLengthException 311 | MemoryContents.MemoryFileChecksumException 312 | MemoryContents.MemoryFileUnknownRecordType 313 | MemoryContents.MemoryFileRecordContentException 314 | MemoryContents.MemoryFileAddressingRangeException 315 | MemoryContents.MemoryFileNoDataRecordsException 316 | MemoryContents.MemoryFileNoEOFRecordException 317 | MemoryContents.MemoryFileRecordFoundAfterEOFRecord f) { 318 log.error(f.getLocalizedMessage()); 319 status.setText(Bundle.getMessage("ErrorFileContentsError")); 320 this.disableDownloadVerifyButtons(); 321 return; 322 } catch (IOException e) { 323 log.error(e.getLocalizedMessage()); 324 status.setText(Bundle.getMessage("ErrorFileReadError")); 325 this.disableDownloadVerifyButtons(); 326 return; 327 } 328 329 log.debug("Read complete: {}", inputContent.toString()); 330 331 loadButton.setEnabled(true); 332 loadButton.setToolTipText(Bundle.getMessage("TipLoadEnabled")); 333 verifyButton.setEnabled(true); 334 verifyButton.setToolTipText(Bundle.getMessage("TipVerifyEnabled")); 335 status.setText(Bundle.getMessage("StatusDoDownload")); 336 337 handleOptionsInFileContent(inputContent); 338 339 MemoryContents.LoadOffsetFieldType addresstype = inputContent.getCurrentAddressFormat(); 340 if (addresstype == MemoryContents.LoadOffsetFieldType.ADDRESSFIELDSIZE16BITS) { 341 address16bit.setSelected(true); 342 address24bit.setSelected(false); 343 } else if (addresstype == MemoryContents.LoadOffsetFieldType.ADDRESSFIELDSIZE24BITS) { 344 address16bit.setSelected(false); 345 address24bit.setSelected(true); 346 } 347 if (!parametersAreValid()) { 348 status.setText(Bundle.getMessage("ErrorInvalidParameter")); 349 disableDownloadVerifyButtons(); 350 } else if (!inputContent.isEmpty()) { 351 enableDownloadVerifyButtons(); 352 } 353 } 354 355 protected void doLoad() { 356 status.setText(Bundle.getMessage("StatusDownloading")); 357 loadButton.setEnabled(false); 358 loadButton.setToolTipText(Bundle.getMessage("TipDisabledDownload")); 359 verifyButton.setEnabled(false); 360 verifyButton.setToolTipText(Bundle.getMessage("TipDisabledDownload")); 361 abortButton.setEnabled(true); 362 abortButton.setToolTipText(Bundle.getMessage("TipAbortEnabled")); 363 selectButton.setEnabled(false); 364 } 365 366 protected void doVerify() { 367 status.setText(Bundle.getMessage("StatusVerifying")); 368 loadButton.setEnabled(false); 369 loadButton.setToolTipText(Bundle.getMessage("TipDisabledDownload")); 370 verifyButton.setEnabled(false); 371 verifyButton.setToolTipText(Bundle.getMessage("TipDisabledDownload")); 372 abortButton.setEnabled(true); 373 abortButton.setToolTipText(Bundle.getMessage("TipAbortEnabled")); 374 selectButton.setEnabled(false); 375 } 376 377 /** 378 * Cleans up the GUI interface. Updates status line to a localized "done" 379 * message or a localized "aborted" message depending on the value returned 380 * by isOperationAborted() . Assumes that the file was properly read to 381 * memory and is usable for firmware update and/or verify operations, and 382 * configures the Load, and Verify GUI buttons as enabled, and the Abort GUI 383 * button as disabled. 384 * 385 */ 386 protected void enableDownloadVerifyButtons() { 387 log.debug("enableGUI"); 388 389 if (isOperationAborted()) { 390 status.setText(Bundle.getMessage("StatusAbort")); 391 } else { 392 status.setText(Bundle.getMessage("StatusDone")); 393 } 394 395 setOperationAborted(false); 396 397 loadButton.setEnabled(true); 398 loadButton.setToolTipText(Bundle.getMessage("TipLoadEnabled")); 399 verifyButton.setEnabled(true); 400 verifyButton.setToolTipText(Bundle.getMessage("TipVerifyEnabled")); 401 abortButton.setEnabled(false); 402 abortButton.setToolTipText(Bundle.getMessage("TipAbortDisabled")); 403 selectButton.setEnabled(true); 404 } 405 406 /** 407 * Cleans up the GUI interface after a firmware file read fails. Assumes 408 * that the invoking code will update the GUI status line as appropriate for 409 * the particular cause of failure. Configures the Load, Verify and Abort 410 * GUI buttons as disabled. 411 * 412 */ 413 protected void disableDownloadVerifyButtons() { 414 log.debug("disableGUI"); 415 416 setOperationAborted(false); 417 418 loadButton.setEnabled(false); 419 loadButton.setToolTipText(Bundle.getMessage("TipLoadDisabled")); 420 verifyButton.setEnabled(false); 421 verifyButton.setToolTipText(Bundle.getMessage("TipVerifyDisabled")); 422 abortButton.setEnabled(false); 423 abortButton.setToolTipText(Bundle.getMessage("TipAbortDisabled")); 424 selectButton.setEnabled(true); 425 426 } 427 428 // boolean used to abort the threaded operation 429 // access has to be synchronized to make sure 430 // the Sender threads sees the value change from the 431 // GUI thread 432 protected boolean abortOperation; 433 434 protected void setOperationAborted(boolean state) { 435 synchronized (this) { 436 abortOperation = state; 437 } 438 } 439 440 protected boolean isOperationAborted() { 441 synchronized (this) { 442 return abortOperation; 443 } 444 } 445 446 protected void setDefaultFieldValues() { 447 } 448 449 /** 450 * Checks the values in the GUI text boxes to determine if any are invalid. 451 * Intended for use immediately after reading a firmware file for the 452 * purpose of validating any key/value pairs found in the file. Also 453 * intended for use immediately before a "verify" or "download" operation to 454 * check that the user has not changed any of the GUI text values to ones 455 * that are unsupported. 456 * 457 * Note that this method cannot guarantee that the values are suitable for 458 * the hardware being updated and/or for the particular firmware information 459 * which was read from the firmware file. 460 * 461 * @return false if one or more GUI text box contains an invalid value 462 */ 463 protected boolean parametersAreValid() { 464 return true; 465 } 466 467 protected boolean intParameterIsValid(JTextField jtf, int minOk, int maxOk) { 468 String text; 469 int junk; 470 boolean allIsOk = true; 471 jtf.setForeground(Color.black); 472 text = jtf.getText(); 473 if (text.equals("")) { 474 jtf.setText("0"); 475 jtf.setForeground(Color.red); 476 allIsOk = false; 477 } else { 478 try { 479 junk = Integer.parseInt(text); 480 } catch (NumberFormatException ex) { 481 junk = -1; 482 } 483 if ((junk < minOk) || (junk > maxOk)) { 484 jtf.setForeground(Color.red); 485 allIsOk = false; 486 } else { 487 jtf.setForeground(Color.black); 488 } 489 } 490 jtf.updateUI(); 491 return allIsOk; 492 } 493 494 /** 495 * Conditionally enables or disables the Download and Verify GUI buttons 496 * based on the validity of the parameter values in the GUI and the state of 497 * the memory contents object. 498 */ 499 protected void updateDownloadVerifyButtons() { 500 if (parametersAreValid() && !inputContent.isEmpty()) { 501 enableDownloadVerifyButtons(); 502 } else { 503 disableDownloadVerifyButtons(); 504 } 505 } 506 507 public void clearInputFileName() { 508 inputFileName.setText(""); 509 inputFileName.setToolTipText(""); 510 } 511 512 /** 513 * {@inheritDoc} 514 */ 515 @Override 516 public void actionPerformed(ActionEvent e) { 517 updateDownloadVerifyButtons(); 518 log.info("ActionListener"); 519 } 520 521 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractLoaderPane.class); 522 523}