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}