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}