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