001package jmri.jmrit.simpleclock;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.Container;
006import java.awt.event.ActionEvent;
007import java.awt.event.FocusAdapter;
008import java.awt.event.FocusEvent;
009import java.awt.event.WindowEvent;
010import java.beans.PropertyChangeEvent;
011import java.beans.PropertyChangeListener;
012import java.text.DecimalFormat;
013import java.util.Calendar;
014import java.util.Date;
015
016import javax.annotation.CheckForNull;
017import javax.swing.*;
018import jmri.InstanceManager;
019import jmri.Timebase;
020import jmri.TimebaseRateException;
021import jmri.util.JmriJFrame;
022import jmri.util.swing.JmriJOptionPane;
023
024/**
025 * Frame for user configuration of Simple Timebase.
026 * <p>
027 * The current implementation (2007) handles the internal clock and one hardware
028 * clock.
029 *
030 * @author Dave Duchamp Copyright (C) 2004, 2007
031 */
032public class SimpleClockFrame extends JmriJFrame implements PropertyChangeListener {
033
034    private Timebase clock;
035    private String hardwareName = null;
036    private boolean changed = false;
037    protected boolean showTime = false;
038    private final DecimalFormat threeDigits = new DecimalFormat("0.000"); // 3 digit precision for speedup factor
039    private boolean checkEnabled = jmri.InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).isStoreCheckEnabled();
040
041    protected JComboBox<String> timeSourceBox = null;
042    protected JComboBox<String> clockStartBox = null;
043    protected JComboBox<String> startRunBox = null;
044    // These are the indexes into the start run box.
045    private final static int START_RUNNING = 0;
046    private final static int START_STOPPED = 1;
047    private final static int START_NORUNCHANGE = 2;
048
049    protected JCheckBox synchronizeCheckBox = null;
050    protected JCheckBox correctCheckBox = null;
051    protected JCheckBox displayCheckBox = null;
052    protected JCheckBox startSetTimeCheckBox = null;
053    protected JCheckBox startSetRateCheckBox = null;
054    protected JCheckBox displayStartStopButton = null;
055
056    protected JTextField factorField = new JTextField(5);
057    protected JTextField startFactorField = new JTextField(5);
058    protected JTextField hoursField = new JTextField(2);
059    protected JTextField minutesField = new JTextField(2);
060    protected JTextField startHoursField = new JTextField(2);
061    protected JTextField startMinutesField = new JTextField(2);
062
063    protected JButton setRateButton = new JButton(Bundle.getMessage("ButtonSet"));
064    protected JButton setTimeButton = new JButton(Bundle.getMessage("ButtonSet"));
065    protected JButton startButton = new JButton(Bundle.getMessage("ButtonStart"));
066    protected JButton stopButton = new JButton(Bundle.getMessage("ButtonStop"));
067    protected JButton doneButton = new JButton(Bundle.getMessage("ButtonDone"));
068
069    protected JLabel clockStatus = new JLabel();
070    protected JLabel timeLabel = new JLabel();
071
072    private final int internalSourceIndex = 0;
073    private final int hardwareSourceIndex = 1;
074
075    private final int startNone = 0;
076    private final int startNixieClock = 1;
077    private final int startAnalogClock = 2;
078    private final int startLcdClock = 3;
079    private final int startPragotronClock = 4 ;
080
081    /**
082     * Constructor method.
083     */
084    public SimpleClockFrame() {
085        super();
086    }
087
088    /**
089     * Initialize the Clock config window.
090     */
091    @Override
092    public void initComponents() {
093        setTitle(Bundle.getMessage("SimpleClockWindowTitle"));
094
095        // Determine current state of the clock
096        clock = InstanceManager.getNullableDefault(jmri.Timebase.class);
097        if (clock == null) {
098            // could not initialize clock
099            log.error("Could not obtain a Timebase instance.");
100            setVisible(false);
101            dispose();
102        }
103        if (!clock.getIsInitialized()) {
104            // if clocks have not been initialized at start up, do so now
105            clock.initializeHardwareClock();
106        }
107
108        Container contentPane = getContentPane();
109        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));
110
111        contentPane.add(getClockStatePanel());
112
113        JPanel saveContainerPanel = new JPanel();
114        saveContainerPanel.setBorder( BorderFactory.createRaisedBevelBorder() );
115        saveContainerPanel.setLayout(new BoxLayout(saveContainerPanel, BoxLayout.Y_AXIS));
116
117        saveContainerPanel.add(getSourcePane());
118        saveContainerPanel.add(getStartupOptionsPane());
119
120        // add Done button
121        JPanel panel4 = new JPanel();
122        panel4.setLayout(new BoxLayout(panel4, BoxLayout.X_AXIS));
123        panel4.add(doneButton);
124        doneButton.addActionListener(this::doneButtonActionPerformed);
125        saveContainerPanel.add(panel4);
126
127
128        contentPane.add(saveContainerPanel);
129
130        // update contents for current status
131        updateRunningButton();
132
133        // add save menu item
134        JMenuBar menuBar = new JMenuBar();
135        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
136        menuBar.add(fileMenu);
137        fileMenu.add(new jmri.configurexml.StoreMenu());
138
139        setJMenuBar(menuBar);
140        // add help menu to window
141        addHelpMenu("package.jmri.jmrit.simpleclock.SimpleClockFrame", true);
142
143        // pack for display
144        pack();
145
146        // listen for changes to the timebase parameters
147        clock.addPropertyChangeListener(this);
148    }
149
150    private JPanel getClockStatePanel() {
151
152            // Set up clock information panel
153        JPanel clockStatePanel = new JPanel();
154        clockStatePanel.setLayout(new BoxLayout(clockStatePanel, BoxLayout.Y_AXIS));
155        clockStatePanel.setBorder(BorderFactory.createTitledBorder(
156            Bundle.getMessage("BoxLabelClockState")));
157
158        JPanel panel31 = new JPanel();
159        panel31.add(clockStatus);
160
161        JPanel panel32 = new JPanel();
162        panel32.add(new JLabel(Bundle.getMessage("CurrentTime") + " "));
163        setTimeLabel();
164        panel32.add(timeLabel);
165        clockStatePanel.add(panel32);
166
167        // Set up Start and Stop buttons
168        startButton.setToolTipText(Bundle.getMessage("TipStartButton"));
169        startButton.addActionListener(this::startButtonActionPerformed);
170        panel31.add(startButton);
171        stopButton.setToolTipText(Bundle.getMessage("TipStopButton"));
172        stopButton.addActionListener(this::stopButtonActionPerformed);
173        panel31.add(stopButton);
174        clockStatePanel.add(panel31);
175
176        // Set up time setup information
177        JPanel panel2 = new JPanel();
178        panel2.add(new JLabel(Bundle.getMessage("NewTime") + " "));
179        panel2.add(hoursField);
180        hoursField.setText("00");
181        hoursField.setToolTipText(Bundle.getMessage("TipHoursField"));
182        panel2.add(new JLabel(":"));
183        panel2.add(minutesField);
184        minutesField.setText("00");
185        minutesField.setToolTipText(Bundle.getMessage("TipMinutesField"));
186        setTimeButton.setToolTipText(Bundle.getMessage("TipSetTimeButton"));
187        setTimeButton.addActionListener(this::setTimeButtonActionPerformed);
188        panel2.add(setTimeButton);
189        clockStatePanel.add(panel2);
190
191        // Set up speed up factor
192        JPanel panel12 = new JPanel();
193        panel12.add(new JLabel(Bundle.getMessage("SpeedUpFactor") + " "));
194        panel12.add(factorField);
195        factorField.setText(threeDigits.format(clock.userGetRate()));
196        factorField.setToolTipText(Bundle.getMessage("TipFactorField"));
197        panel12.add(new JLabel(":1 "));
198        setRateButton.setToolTipText(Bundle.getMessage("TipSetRateButton"));
199        setRateButton.addActionListener(this::setRateButtonActionPerformed);
200        panel12.add(setRateButton);
201        clockStatePanel.add(panel12);
202
203        JPanel clockStatePanelContainer = new JPanel();
204        clockStatePanelContainer.setBorder( BorderFactory.createRaisedBevelBorder() );
205        clockStatePanelContainer.setLayout(new BoxLayout(clockStatePanelContainer, BoxLayout.X_AXIS));
206        clockStatePanelContainer.add(clockStatePanel);
207        return clockStatePanelContainer;
208
209    }
210
211    private JPanel getSourcePane(){
212
213        JPanel sourcePanel = new JPanel();
214        sourcePanel.setBorder( BorderFactory.createTitledBorder( Bundle.getMessage("TimeSource")));
215        sourcePanel.setLayout(new BoxLayout(sourcePanel, BoxLayout.Y_AXIS));
216
217        // Set up time source choice
218        JPanel panel11 = new JPanel();
219        // panel11.add(new JLabel(Bundle.getMessage("TimeSource") + " "));
220        timeSourceBox = new JComboBox<>();
221        panel11.add(timeSourceBox);
222        timeSourceBox.addItem(Bundle.getMessage("ComputerClock"));
223        hardwareName = InstanceManager.getDefault(jmri.ClockControl.class).getHardwareClockName();
224        if (hardwareName != null) {
225            timeSourceBox.addItem(hardwareName);
226        }
227        timeSourceBox.setToolTipText(Bundle.getMessage("TipTimeSource"));
228        timeSourceBox.addActionListener(this::setTimeSourceChanged);
229        sourcePanel.add(panel11);
230
231        if (hardwareName != null) {
232            timeSourceBox.setSelectedIndex(clock.getInternalMaster() ? internalSourceIndex : hardwareSourceIndex);
233            JPanel panel11x = new JPanel();
234            synchronizeCheckBox = new JCheckBox(Bundle.getMessage("Synchronize") + " "
235                    + hardwareName);
236            synchronizeCheckBox.setToolTipText(Bundle.getMessage("TipSynchronize"));
237            synchronizeCheckBox.setSelected(clock.getSynchronize());
238            synchronizeCheckBox.addActionListener(this::synchronizeChanged);
239            panel11x.add(synchronizeCheckBox);
240            sourcePanel.add(panel11x);
241            if (InstanceManager.getDefault(jmri.ClockControl.class).canCorrectHardwareClock()) {
242                JPanel panel11y = new JPanel();
243                correctCheckBox = new JCheckBox(Bundle.getMessage("Correct"));
244                correctCheckBox.setToolTipText(Bundle.getMessage("TipCorrect"));
245                correctCheckBox.setSelected(clock.getCorrectHardware());
246                correctCheckBox.addActionListener(this::correctChanged);
247                panel11y.add(correctCheckBox);
248                sourcePanel.add(panel11y);
249            }
250            if (InstanceManager.getDefault(jmri.ClockControl.class).canSet12Or24HourClock()) {
251                JPanel panel11z = new JPanel();
252                displayCheckBox = new JCheckBox(Bundle.getMessage("Display12Hour"));
253                displayCheckBox.setToolTipText(Bundle.getMessage("TipDisplay"));
254                displayCheckBox.setSelected(clock.use12HourDisplay());
255                displayCheckBox.addActionListener(this::displayChanged);
256                panel11z.add(displayCheckBox);
257                sourcePanel.add(panel11z);
258            }
259        }
260
261        return sourcePanel;
262
263    }
264
265    private JPanel getStartupOptionsPane(){
266
267        // Set up startup options panel
268        JPanel startupOptionsPane = new JPanel();
269        startupOptionsPane.setLayout(new BoxLayout(startupOptionsPane, BoxLayout.Y_AXIS));
270        JPanel panel61 = new JPanel();
271        panel61.add(new JLabel(Bundle.getMessage("StartBoxLabel") + " "));
272        startRunBox = new JComboBox<>();
273        startRunBox.addItem(Bundle.getMessage("StartSelectRunning"));
274        startRunBox.addItem(Bundle.getMessage("StartSelectStopped"));
275        startRunBox.addItem(Bundle.getMessage("StartSelectNoChange"));
276        startRunBox.setToolTipText(Bundle.getMessage("TipStartRunSelect"));
277        switch (clock.getClockInitialRunState()) {
278            case DO_STOP:
279                startRunBox.setSelectedIndex(START_STOPPED);
280                break;
281            case DO_START:
282                startRunBox.setSelectedIndex(START_RUNNING);
283                break;
284            case DO_NOTHING:
285                startRunBox.setSelectedIndex(START_NORUNCHANGE);
286                break;
287            default:
288                jmri.util.LoggingUtil.warnOnce(log, "Unexpected initial run state = {}", clock.getClockInitialRunState());
289                break;
290        }
291        startRunBox.addActionListener(this::startRunBoxChanged);
292        panel61.add(startRunBox);
293        startupOptionsPane.add(panel61);
294
295        JPanel panel62 = new JPanel();
296        startSetTimeCheckBox = new JCheckBox(Bundle.getMessage("StartSetTime"));
297        startSetTimeCheckBox.setToolTipText(Bundle.getMessage("TipStartSetTime"));
298        startSetTimeCheckBox.setSelected(clock.getStartSetTime());
299        startSetTimeCheckBox.addActionListener(this::startSetTimeChanged);
300        panel62.add(startSetTimeCheckBox);
301        Calendar cal = Calendar.getInstance();
302        cal.setTime(clock.getStartTime());
303        startHoursField.setText("" + cal.get(Calendar.HOUR_OF_DAY));
304        startHoursField.setToolTipText(Bundle.getMessage("TipStartHours"));
305        panel62.add(startHoursField);
306        panel62.add(new JLabel(":"));
307        startMinutesField.setText("" + cal.get(Calendar.MINUTE));
308        startMinutesField.setToolTipText(Bundle.getMessage("TipStartMinutes"));
309        panel62.add(startMinutesField);
310
311        startMinutesField.addFocusListener(getStartUpSetTimeChangedAdapter());
312        startHoursField.addFocusListener(getStartUpSetTimeChangedAdapter());
313        startupOptionsPane.add(panel62);
314
315        JPanel panelStartSetRate = new JPanel();
316        startSetRateCheckBox = new JCheckBox(Bundle.getMessage("StartSetSpeedUpFactor") + " ");
317        startSetRateCheckBox.setToolTipText(Bundle.getMessage("TipStartSetRate"));
318        startSetRateCheckBox.setSelected(clock.getSetRateAtStart());
319        startSetRateCheckBox.addActionListener(this::startSetRateChanged);
320        panelStartSetRate.add(startSetRateCheckBox);
321        panelStartSetRate.add(startFactorField);
322        startFactorField.setText(threeDigits.format(clock.getStartRate()));
323        startFactorField.setToolTipText(Bundle.getMessage("TipFactorField"));
324        startFactorField.addActionListener(this::startFactorFieldChanged);
325        startFactorField.addFocusListener(new FocusAdapter() {
326            @Override
327            public void focusLost(FocusEvent focusEvent) {
328                if (!focusEvent.isTemporary()) {
329                    startFactorFieldChanged(null);
330                }
331                super.focusLost(focusEvent);
332            }
333        });
334        panelStartSetRate.add(new JLabel(":1 "));
335        startupOptionsPane.add(panelStartSetRate);
336
337        JPanel panel63 = new JPanel();
338        panel63.add(new JLabel(Bundle.getMessage("StartClock") + " "));
339        clockStartBox = new JComboBox<>();
340        panel63.add(clockStartBox);
341        clockStartBox.addItem(Bundle.getMessage("None"));
342        clockStartBox.addItem(Bundle.getMessage("MenuItemNixieClock"));
343        clockStartBox.addItem(Bundle.getMessage("MenuItemAnalogClock"));
344        clockStartBox.addItem(Bundle.getMessage("MenuItemLcdClock"));
345        clockStartBox.addItem(Bundle.getMessage("MenuItemPragotronClock"));
346        clockStartBox.setSelectedIndex(startNone);
347        if (clock.getStartClockOption() == Timebase.NIXIE_CLOCK) {
348            clockStartBox.setSelectedIndex(startNixieClock);
349        } else {
350            if (clock.getStartClockOption() == Timebase.ANALOG_CLOCK) {
351                clockStartBox.setSelectedIndex(startAnalogClock);
352            } else {
353                if (clock.getStartClockOption() == Timebase.LCD_CLOCK) {
354                    clockStartBox.setSelectedIndex(startLcdClock);
355                } else {
356                    if (clock.getStartClockOption() == Timebase.PRAGOTRON_CLOCK) {
357                        clockStartBox.setSelectedIndex(startPragotronClock);
358                    }
359                }
360            }
361        }
362        clockStartBox.setToolTipText(Bundle.getMessage("TipClockStartOption"));
363        clockStartBox.addActionListener(this::setClockStartChanged);
364        startupOptionsPane.add(panel63);
365        JPanel panel64 = new JPanel();
366        displayStartStopButton= new JCheckBox(Bundle.getMessage("DisplayOnOff"));
367        displayStartStopButton.setSelected(clock.getShowStopButton());
368        displayStartStopButton.addActionListener(this::showStopButtonChanged);
369        panel64.add(displayStartStopButton);
370        startupOptionsPane.add(panel64);
371
372        startupOptionsPane.setBorder(BorderFactory.createTitledBorder(
373                Bundle.getMessage("BoxLabelStartUp")));
374
375        return startupOptionsPane;
376
377    }
378
379    private FocusAdapter getStartUpSetTimeChangedAdapter(){
380        return new FocusAdapter() {
381            @Override
382            public void focusLost(FocusEvent focusEvent) {
383                if (!focusEvent.isTemporary()) {
384                    startSetTimeChanged(null);
385                }
386                super.focusLost(focusEvent);
387            }
388        };
389    }
390
391    private void startFactorFieldChanged(ActionEvent e) {
392        Double v = parseRate(startFactorField.getText());
393        if (v != null && !v.equals(clock.getStartRate())) {
394            clock.setStartRate(v);
395            changed = true;
396        }
397        startFactorField.setText(threeDigits.format(clock.getStartRate()));
398    }
399
400    private void startSetRateChanged(ActionEvent e) {
401        clock.setSetRateAtStart(startSetRateCheckBox.isSelected());
402        changed = true;
403    }
404
405    /**
406     * Adjust to running state changes
407     */
408    void updateRunningButton() {
409        boolean running = clock.getRun();
410        if (running) {
411            clockStatus.setText(Bundle.getMessage("ClockRunning"));
412            startButton.setVisible(false);
413            stopButton.setVisible(true);
414        } else {
415            clockStatus.setText(Bundle.getMessage("ClockStopped"));
416            startButton.setVisible(true);
417            stopButton.setVisible(false);
418        }
419        clockStatus.setVisible(true);
420    }
421
422    /**
423     * Converts a user-entered rate to a double, possibly throwing up warning dialogs.
424     * @param fieldEntry value from text field where the user entered a rate.
425     * @return null if the rate could not be parsed, negative, or an unsupported fraction.
426     * Otherwise the fraction value.
427     */
428    @CheckForNull
429    Double parseRate(String fieldEntry) {
430        double rate;
431        try {
432            char decimalSeparator = threeDigits.getDecimalFormatSymbols().getDecimalSeparator() ;
433            if (decimalSeparator != '.') {
434                fieldEntry = fieldEntry.replace(decimalSeparator, '.') ;
435            }
436            rate = Double.parseDouble(fieldEntry);
437        } catch (NumberFormatException e) {
438            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("ParseRateError") + "\n" + e),
439                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
440            log.error("Exception when parsing user-entered rate", e);
441            return null;
442        }
443        if (rate < 0.0) {
444            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("NegativeRateError"),
445                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
446            return null;
447        }
448        if (InstanceManager.getDefault(jmri.ClockControl.class).requiresIntegerRate() && !clock.getInternalMaster()) {
449            double frac = rate - (int) rate;
450            if (frac > 0.001) {
451                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("NonIntegerError"),
452                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
453                return null;
454            }
455        }
456        return rate;
457    }
458
459    /**
460     * Handle Set Rate button.
461     * @param ev unused
462     */
463    public void setRateButtonActionPerformed(ActionEvent ev) {
464        Double parsedRate = parseRate(factorField.getText());
465        if (parsedRate == null) {
466            factorField.setText(threeDigits.format(clock.userGetRate()));
467            return;
468        }
469        try {
470            clock.userSetRate(parsedRate);
471        } catch (TimebaseRateException e) {
472            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("SetRateError") + "\n" + e.getLocalizedMessage()),
473                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
474            // log.error is already called by clock.userSetRate
475        }
476        changed = true;
477    }
478
479    /**
480     * Handle time source change
481     *
482     * Only changes the time source if the rate is OK (typically: Integer) for new source
483     */
484    private void setTimeSourceChanged(ActionEvent e) {
485        int index = timeSourceBox.getSelectedIndex();
486        int oldIndex = internalSourceIndex;
487        if (!clock.getInternalMaster()) {
488            oldIndex = hardwareSourceIndex;
489        }
490        // return if nothing changed
491        if (oldIndex == index) {
492            return;
493        }
494        // change the time source master
495        if (index == internalSourceIndex) {
496            clock.setInternalMaster(true, true);
497        } else {
498            // only change if new source is okay with current rate
499            if (InstanceManager.getDefault(jmri.ClockControl.class).requiresIntegerRate()) {
500                double rate = clock.userGetRate();
501                double frac = rate - (int) rate;
502                if (frac > 0.001) {
503                    JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("NonIntegerErrorCantChangeSource"),
504                            Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
505                    timeSourceBox.setSelectedIndex(internalSourceIndex);
506                    return;
507                }
508            }
509            clock.setInternalMaster(false, true);
510        }
511        changed = true;
512    }
513
514    /**
515     * Handle synchronize check box change
516     */
517    private void synchronizeChanged(ActionEvent e) {
518        clock.setSynchronize(synchronizeCheckBox.isSelected(), true);
519        changed = true;
520    }
521
522    /**
523     * Handle correct check box change
524     */
525    private void correctChanged(ActionEvent e) {
526        clock.setCorrectHardware(correctCheckBox.isSelected(), true);
527        changed = true;
528    }
529
530    /**
531     * Handle 12-hour display check box change
532     */
533    private void displayChanged(ActionEvent e) {
534        clock.set12HourDisplay(displayCheckBox.isSelected(), true);
535        changed = true;
536    }
537
538    /**
539     * Handle Set Time button.
540     * @param ex unused
541     */
542    public void setTimeButtonActionPerformed(ActionEvent ex) {
543        int hours;
544        int minutes;
545        // get hours, reporting errors if any
546        try {
547            hours = Integer.parseInt(hoursField.getText());
548        } catch (NumberFormatException e) {
549            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("HoursError") + "\n" + e),
550                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
551            log.error("Exception when parsing hours Field", e);
552            return;
553        }
554        if ((hours < 0) || (hours > 23)) {
555            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("HoursRangeError")),
556                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
557            return;
558        }
559        // get minutes, reporting errors if any
560        try {
561            minutes = Integer.parseInt(minutesField.getText());
562        } catch (NumberFormatException e) {
563            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("MinutesError") + "\n" + e),
564                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
565            log.error("Exception when parsing Minutes Field", e);
566            return;
567        }
568        if ((minutes < 0) || (minutes > 59)) {
569            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("MinutesRangeError")),
570                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
571            return;
572        }
573        // set time of the fast clock
574        long mSecPerHour = 3600000;
575        long mSecPerMinute = 60000;
576        Calendar cal = Calendar.getInstance();
577        cal.setTime(clock.getTime());
578        int cHours = cal.get(Calendar.HOUR_OF_DAY);
579        long cNumMSec = cal.getTime().getTime();
580        long nNumMSec = ((cNumMSec / mSecPerHour) * mSecPerHour) - (cHours * mSecPerHour)
581                + (hours * mSecPerHour) + (minutes * mSecPerMinute);
582        clock.userSetTime(new Date(nNumMSec));
583        showTime = true;
584        updateTime();
585    }
586
587    /**
588     * Handle start run combo box change
589     */
590    private void startRunBoxChanged(ActionEvent e) {
591        switch (startRunBox.getSelectedIndex()) {
592            case START_STOPPED:
593                clock.setClockInitialRunState(Timebase.ClockInitialRunState.DO_STOP);
594                break;
595            case START_RUNNING:
596                clock.setClockInitialRunState(Timebase.ClockInitialRunState.DO_START);
597                break;
598            default:
599            case START_NORUNCHANGE:
600                clock.setClockInitialRunState(Timebase.ClockInitialRunState.DO_NOTHING);
601                break;
602        }
603        changed = true;
604    }
605
606    /**
607     * Handle Show on/off button check box change
608     */
609    private void showStopButtonChanged(ActionEvent e) {
610        clock.setShowStopButton(displayStartStopButton.isSelected());
611        changed = true;
612    }
613
614    /**
615     * Handle start set time check box change
616     */
617    private void startSetTimeChanged(ActionEvent ev) {
618        int hours;
619        int minutes;
620        // get hours, reporting errors if any
621        try {
622            hours = Integer.parseInt(startHoursField.getText());
623        } catch (NumberFormatException e) {
624            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("HoursError") + "\n" + e),
625                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
626            log.error("Exception when parsing hours Field", e);
627            return;
628        }
629        if ((hours < 0) || (hours > 23)) {
630            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("HoursRangeError")),
631                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
632            return;
633        }
634        // get minutes, reporting errors if any
635        try {
636            minutes = Integer.parseInt(startMinutesField.getText());
637        } catch (NumberFormatException e) {
638            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("MinutesError") + "\n" + e),
639                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
640            log.error("Exception when parsing Minutes Field", e);
641            return;
642        }
643        if ((minutes < 0) || (minutes > 59)) {
644            JmriJOptionPane.showMessageDialog(this, (Bundle.getMessage("MinutesRangeError")),
645                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
646            return;
647        }
648        // set time of the fast clock
649        long mSecPerHour = 3600000;
650        long mSecPerMinute = 60000;
651        Calendar cal = Calendar.getInstance();
652        int cHours = cal.get(Calendar.HOUR_OF_DAY);
653        long cNumMSec = cal.getTime().getTime();
654        long nNumMSec = ((cNumMSec / mSecPerHour) * mSecPerHour) - (cHours * mSecPerHour)
655                + (hours * mSecPerHour) + (minutes * mSecPerMinute);
656        clock.setStartSetTime(startSetTimeCheckBox.isSelected(), new Date(nNumMSec));
657        changed = true;
658    }
659
660    /**
661     * Handle start clock combo box change
662     */
663    private void setClockStartChanged(ActionEvent e) {
664        int sel = Timebase.NONE;
665        switch (clockStartBox.getSelectedIndex()) {
666            case startNixieClock:
667                sel = Timebase.NIXIE_CLOCK;
668                break;
669            case startAnalogClock:
670                sel = Timebase.ANALOG_CLOCK;
671                break;
672            case startLcdClock:
673                sel = Timebase.LCD_CLOCK;
674                break;
675            case startPragotronClock:
676                sel = Timebase.PRAGOTRON_CLOCK;
677                break;
678            default:
679                break;
680        }
681        clock.setStartClockOption(sel);
682        changed = true;
683    }
684
685    /**
686     * Handle Start Clock button
687     * @param e unused
688     */
689    public void startButtonActionPerformed(ActionEvent e) {
690        clock.setRun(true);
691    }
692
693    /**
694     * Handle Stop Clock button.
695     * @param e unused
696     */
697    public void stopButtonActionPerformed(ActionEvent e) {
698        clock.setRun(false);
699    }
700
701    /**
702     * Update clock state information
703     */
704    void updateTime() {
705        if (clock.getRun() || showTime) {
706            showTime = false;
707            setTimeLabel();
708            timeLabel.setVisible(true);
709        }
710    }
711
712    /**
713     * Set the current Timebase time into timeLabel
714     */
715    void setTimeLabel() {
716        // Get time
717        Calendar cal = Calendar.getInstance();
718        cal.setTime(clock.getTime());
719        int hours = cal.get(Calendar.HOUR_OF_DAY);
720        int minutes = cal.get(Calendar.MINUTE);
721        // Format and display the time
722        timeLabel.setText(" " + (hours / 10) + (hours - (hours / 10) * 10) + ":"
723                + (minutes / 10) + (minutes - (minutes / 10) * 10));
724        timeLabel.setToolTipText(clock.getTime().toString());
725    }
726
727    /**
728     * Handle a change to clock properties.
729     * {@inheritDoc}
730     */
731    @Override
732    public void propertyChange(PropertyChangeEvent event) {
733        log.trace("propertyChange({})",event.getPropertyName());
734        switch (event.getPropertyName()) {
735            case "run":
736                updateRunningButton();
737                break;
738            case "rate":
739                factorField.setText(threeDigits.format(clock.userGetRate()));
740                break;
741            case "time":
742                updateTime();
743                break;
744            case "config": //indicates that something in the clock config has changed, so update all
745                synchronizeCheckBox.setSelected(clock.getSynchronize());
746                timeSourceBox.setSelectedIndex(clock.getInternalMaster() ? internalSourceIndex : hardwareSourceIndex);
747                switch (clock.getClockInitialRunState()) {
748                    case DO_STOP:
749                        startRunBox.setSelectedIndex(START_STOPPED);
750                        break;
751                    case DO_START:
752                        startRunBox.setSelectedIndex(START_RUNNING);
753                        break;
754                    case DO_NOTHING:
755                        startRunBox.setSelectedIndex(START_NORUNCHANGE);
756                        break;
757                    default:
758                        jmri.util.LoggingUtil.warnOnce(log, "Unexpected initial run state = {}", clock.getClockInitialRunState());
759                        break;
760                }
761                startSetTimeCheckBox.setSelected(clock.getStartSetTime());
762                displayStartStopButton.setSelected(clock.getShowStopButton());
763                startSetRateCheckBox.setSelected(clock.getSetRateAtStart());
764                Calendar cal = Calendar.getInstance();
765                cal.setTime(clock.getStartTime());
766                startHoursField.setText("" + cal.get(Calendar.HOUR_OF_DAY));
767                startMinutesField.setText("" + cal.get(Calendar.MINUTE));
768                startSetRateCheckBox.setSelected(clock.getSetRateAtStart());
769                startFactorField.setText(threeDigits.format(clock.getStartRate()));
770                if (clock.getStartClockOption() == startNone) {
771                    clockStartBox.setSelectedIndex(startNone);
772                } else if (clock.getStartClockOption() == Timebase.NIXIE_CLOCK) {
773                    clockStartBox.setSelectedIndex(startNixieClock);
774                } else if (clock.getStartClockOption() == Timebase.ANALOG_CLOCK) {
775                    clockStartBox.setSelectedIndex(startAnalogClock);
776                } else if (clock.getStartClockOption() == Timebase.LCD_CLOCK) {
777                    clockStartBox.setSelectedIndex(startLcdClock);
778                } else if (clock.getStartClockOption() == Timebase.PRAGOTRON_CLOCK) {
779                    clockStartBox.setSelectedIndex(startPragotronClock);
780                }
781                if (correctCheckBox != null) {
782                    correctCheckBox.setSelected(clock.getCorrectHardware());
783                }
784                break;
785            default:
786                // ignore all other properties
787        }
788    }
789
790    @Override
791    protected void handleModified() {
792        // ignore super routine
793    }
794
795    /**
796     * Handle Done button.
797     * @param e null if a save reminder, not null then from Done button action.
798     */
799    public void doneButtonActionPerformed(ActionEvent e) {
800        if (changed && !checkEnabled) {
801            if (InstanceManager.getNullableDefault(jmri.UserPreferencesManager.class) != null) {
802                InstanceManager.getDefault(jmri.UserPreferencesManager.class).
803                        showInfoMessage(Bundle.getMessage("ReminderTitle"),  // NOI18N
804                        Bundle.getMessage("ReminderSaveString", Bundle.getMessage("SimpleClockWindowTitle")),  // NOI18N
805                        getClassName(),
806                        "remindSaveClock");  // NOI18N
807            }
808        }
809        changed = false;
810        setVisible(false);
811        super.windowClosing(null);
812        dispose();
813    }
814
815    /**
816     * Get the class description for the UserMessagePreferencesPane.
817     * @return The class description
818     */
819    public String getClassDescription() {
820        return "Fast Clock";
821    }
822
823    /**
824     * Set the item details for the UserMessagePreferencesPane.
825     */
826    public void setMessagePreferencesDetails() {
827        InstanceManager.getDefault(jmri.UserPreferencesManager.class).
828                setPreferenceItemDetails(getClassName(), "remindSaveClock", "HideSaveReminder");  // NOI18N
829    }
830
831    protected String getClassName() {
832        // The class that is returned must have a default constructor,
833        // a constructor with no parameters.
834        return this.getClass().getName();
835    }
836
837    /**
838     * If data changed, prompt to store.
839     * {@inheritDoc}
840     */
841    @Override
842    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
843            justification = "This calls doneButtonActionPerformed which calls super.windowClosing()")
844    public void windowClosing(WindowEvent e) {
845        doneButtonActionPerformed(null);
846    }
847
848    @Override
849    public void dispose() {
850        if ( clock != null ) {
851            clock.removePropertyChangeListener(this);
852        }
853        super.dispose();
854    }
855
856    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SimpleClockFrame.class);
857
858}