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