001package jmri.jmrit.progsupport;
002
003import java.awt.event.ActionListener;
004import java.beans.PropertyChangeListener;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009import javax.swing.BoxLayout;
010import javax.swing.ButtonGroup;
011import javax.swing.JCheckBox;
012import javax.swing.JComboBox;
013import javax.swing.JLabel;
014import javax.swing.JPanel;
015import javax.swing.JRadioButton;
016import javax.swing.JSpinner;
017import javax.swing.SpinnerNumberModel;
018import jmri.AddressedProgrammer;
019import jmri.AddressedProgrammerManager;
020import jmri.InstanceManager;
021import jmri.Programmer;
022import jmri.ProgrammingMode;
023import jmri.implementation.AccessoryOpsModeProgrammerFacade;
024import jmri.jmrix.loconet.LnProgrammerManager;
025
026/**
027 * Provide a JPanel to configure the ops programming (Adressed) mode.
028 * <p>
029 * Note that you should call the dispose() method when you're really done, so
030 * that a ProgModePane object can disconnect its listeners.
031 *
032 * @author Bob Jacobsen      Copyright (C) 2001
033 * @author Daniel Bergqvist  Copyright (C) 2021
034 */
035public class ProgOpsModePane extends ProgModeSelector implements PropertyChangeListener, ActionListener {
036
037    // GUI member declarations
038    ButtonGroup modeGroup;
039    HashMap<ProgrammingMode, JRadioButton> buttonMap = new HashMap<>();
040    JComboBox<AddressedProgrammerManager> progBox;
041    ArrayList<JRadioButton> buttonPool = new ArrayList<>();
042    // JTextField mAddrField = new JTextField(4);
043    // use JSpinner for CV number input
044    SpinnerNumberModel model = new SpinnerNumberModel(0, 0, 10239, 1); // 10239 is highest DCC Long Address documented by NMRA as per 2017
045    JSpinner mAddrField = new JSpinner(model);
046    int lowAddrLimit = 0;
047    int highAddrLimit = 10239;
048    int oldAddrValue = 3; // Default start value
049    ButtonGroup addrGroup = new ButtonGroup();
050    JRadioButton shortAddrButton = new JRadioButton(Bundle.getMessage("ShortAddress"));
051    JRadioButton longAddrButton = new JRadioButton(Bundle.getMessage("LongAddress"));
052    JCheckBox offsetAddrCheckBox = new JCheckBox(Bundle.getMessage("DccAccessoryAddressOffSet"));
053    JLabel addressLabel = new JLabel(Bundle.getMessage("AddressLabel"));
054    boolean oldLongAddr = false;
055    boolean opsAccyMode = false;
056    boolean oldOpsAccyMode = false;
057    boolean opsSigMode = false;
058    boolean oldOpsSigMode = false;
059    boolean lnAttachedBoardMode = false;    // LOCONETOPSBOARD programming
060    boolean oldLnAttachedBoardMode = false;
061    boolean lnSv2Mode = false;              // LOCONETSV2MODE programming
062    boolean oldLnSv2Mode = false;
063    boolean lncvMode = false;               // LOCONETLNCVMODE programming
064    boolean oldLncvMode = false;
065    boolean oldoffsetAddrCheckBox = false;
066    transient volatile AddressedProgrammer programmer = null;
067    transient volatile AccessoryOpsModeProgrammerFacade facadeProgrammer = null;
068
069    /**
070     * Get the selected programmer.
071     */
072    @Override
073    public Programmer getProgrammer() {
074        log.debug("getProgrammer mLongAddrCheck.isSelected()={}, oldLongAddr={}, mAddrField.getValue()={}, oldAddrValue={}, opsAccyMode={}, oldOpsAccyMode={}, opsSigMode={}, oldOpsSigMode={}, lnSv2Mode={}, oldLnSv2Mode={}, lncvMode={}, oldLncvMode={}, oldoffsetAddrCheckBox={})",
075                longAddrButton.isSelected(), oldLongAddr, mAddrField.getValue(), oldAddrValue, opsAccyMode, oldOpsAccyMode, opsSigMode, oldOpsSigMode, lnSv2Mode, oldLnSv2Mode, lncvMode, oldLncvMode, oldoffsetAddrCheckBox);
076        if (longAddrButton.isSelected() == oldLongAddr
077                && mAddrField.getValue().equals(oldAddrValue)
078                && offsetAddrCheckBox.isSelected() == oldoffsetAddrCheckBox
079                && opsAccyMode == oldOpsAccyMode
080                && opsSigMode == oldOpsSigMode
081                && lnAttachedBoardMode == oldLnAttachedBoardMode
082                && lnSv2Mode == oldLnSv2Mode
083                && lncvMode == oldLncvMode) {
084            log.debug("getProgrammer hasn't changed");
085            // hasn't changed
086            if (opsAccyMode || opsSigMode) {
087                return facadeProgrammer;
088            } else {
089                return programmer;
090            }
091        }
092
093        // here values have changed, try to create a new one
094        AddressedProgrammerManager pm = ((AddressedProgrammerManager) progBox.getSelectedItem());
095        oldLongAddr = longAddrButton.isSelected();
096        oldAddrValue = (Integer) mAddrField.getValue();
097        oldOpsAccyMode = opsAccyMode;
098        oldOpsSigMode = opsSigMode;
099        oldLnAttachedBoardMode = lnAttachedBoardMode;
100        oldLnSv2Mode = lnSv2Mode;
101        oldLncvMode = lncvMode;
102        oldoffsetAddrCheckBox = offsetAddrCheckBox.isSelected();
103        setAddrParams();
104
105        if (pm != null) {
106            int address = 3;
107            try {
108                address = (Integer) mAddrField.getValue();
109            } catch (java.lang.NumberFormatException e) {
110                log.error("loco address \"{}\" not correct", mAddrField.getValue());
111                programmer = null;
112            }
113            boolean longAddr = longAddrButton.isSelected();
114            log.debug("ops programmer for address {}, long address {}", address, longAddr);
115            programmer = pm.getAddressedProgrammer(longAddr, address);
116            log.debug("   programmer: {}", programmer);
117
118            // whole point is to get mode...
119            setProgrammerFromGui(programmer);
120        } else {
121            log.warn("request for ops mode programmer with no ProgrammerManager configured");
122            programmer = null;
123        }
124        if (opsAccyMode) {
125            log.debug("   getting AccessoryOpsModeProgrammerFacade");
126            facadeProgrammer = new AccessoryOpsModeProgrammerFacade(programmer,
127                    longAddrButton.isSelected() ? "accessory" : "decoder", 200, programmer);
128            return facadeProgrammer;
129        } else if (opsSigMode) {
130            String addrType = offsetAddrCheckBox.isSelected() ? "signal" : "altsignal";
131            log.debug("   getting AccessoryOpsModeProgrammerFacade {}", addrType);
132            facadeProgrammer = new AccessoryOpsModeProgrammerFacade(programmer, addrType, 200, programmer);
133            return facadeProgrammer;
134        }
135        return programmer;
136    }
137
138    /**
139     * Are any of the modes selected?
140     *
141     * @return true is any button is selected
142     */
143    @Override
144    public boolean isSelected() {
145        for (JRadioButton button : buttonMap.values()) {
146            if (button.isSelected()) {
147                return true;
148            }
149        }
150        return false;
151    }
152
153    /**
154     * Constructor for the Programming settings pane.
155     *
156     * @param direction controls layout, either BoxLayout.X_AXIS or
157     *                  BoxLayout.Y_AXIS
158     */
159    public ProgOpsModePane(int direction) {
160        this(direction, new javax.swing.ButtonGroup());
161    }
162
163    /**
164     * Constructor for the Programming settings pane.
165     *
166     * @param direction controls layout, either BoxLayout.X_AXIS or
167     *                  BoxLayout.Y_AXIS
168     * @param group     A set of JButtons to display programming modes
169     */
170    public ProgOpsModePane(int direction, javax.swing.ButtonGroup group) {
171        modeGroup = group;
172        addrGroup.add(shortAddrButton);
173        addrGroup.add(longAddrButton);
174
175        // general GUI config
176        setLayout(new BoxLayout(this, direction));
177
178        setBorder(javax.swing.BorderFactory.createTitledBorder(Bundle.getMessage("TitleProgramOnMain")));
179
180        // create the programmer display combo box
181        List<AddressedProgrammerManager> v = new ArrayList<>(InstanceManager.getList(AddressedProgrammerManager.class));
182        add(progBox = new JComboBox<>(v.toArray(new AddressedProgrammerManager[0])));
183        // if only one, don't show
184        if (progBox.getItemCount() < 2) {
185            progBox.setVisible(false);
186        }
187        progBox.setSelectedItem(InstanceManager.getDefault(jmri.AddressedProgrammerManager.class)); // set default
188        progBox.addActionListener((java.awt.event.ActionEvent e) -> {
189            // new programmer selection
190            programmerSelected();
191        });
192
193        add(new JLabel(" "));
194        add(shortAddrButton);
195        add(longAddrButton);
196        add(offsetAddrCheckBox);
197        offsetAddrCheckBox.setToolTipText(Bundle.getMessage("DccOffsetTooltip"));
198        JPanel panel = new JPanel();
199        panel.setLayout(new java.awt.FlowLayout());
200        panel.add(addressLabel);
201        panel.add(mAddrField);
202        mAddrField.setToolTipText(Bundle.getMessage("ToolTipEnterDecoderAddress"));
203        add(panel);
204        add(new JLabel(Bundle.getMessage("OpsModeLabel")));
205
206//        mAddrField.addActionListener(new java.awt.event.ActionListener() {
207//            @Override
208//            public void actionPerformed(java.awt.event.ActionEvent e) {
209        // new programmer selection
210//                programmerSelected(); // in case it has valid address now
211//            }
212//        });
213        shortAddrButton.addActionListener((java.awt.event.ActionEvent e) -> {
214            // new programmer selection
215            programmerSelected(); // in case it has valid address now
216        });
217
218        longAddrButton.addActionListener((java.awt.event.ActionEvent e) -> {
219            // new programmer selection
220            programmerSelected(); // in case it has valid address now
221        });
222
223        offsetAddrCheckBox.addActionListener((java.awt.event.ActionEvent e) -> {
224            // new programmer selection
225            programmerSelected(); // in case it has valid address now
226        });
227
228        shortAddrButton.setSelected(true);
229        offsetAddrCheckBox.setSelected(false);
230        offsetAddrCheckBox.setVisible(false);
231
232        // and execute the setup for 1st time
233        programmerSelected();
234    }
235
236    /**
237     * Reload the interface with the new programmers.
238     */
239    void programmerSelected() {
240        log.debug("programmerSelected starts with {} buttons", buttonPool.size());
241        // hide buttons
242        for (JRadioButton button : buttonPool) {
243            button.setVisible(false);
244        }
245
246        // clear map
247        buttonMap.clear();
248
249        // require new programmer if possible
250        oldAddrValue = -1;
251
252        // configure buttons
253        int index = 0;
254        List<ProgrammingMode> modes = new ArrayList<>();
255        if (getProgrammer() != null) {
256            modes.addAll(programmer.getSupportedModes());
257        } else if (progBox.getSelectedItem() != null) {
258            modes.addAll(((AddressedProgrammerManager) progBox.getSelectedItem()).getDefaultModes());
259        }
260        // add OPSACCBYTEMODE & OPSACCEXTBYTEMODE if possible
261        if (modes.contains(ProgrammingMode.OPSBYTEMODE)) {
262            if (!modes.contains(ProgrammingMode.OPSACCBYTEMODE)) {
263                log.debug("   adding button for {} via AccessoryOpsModeProgrammerFacade", ProgrammingMode.OPSACCBYTEMODE);
264                modes.add(ProgrammingMode.OPSACCBYTEMODE);
265            }
266            if (!modes.contains(ProgrammingMode.OPSACCEXTBYTEMODE)) {
267                log.debug("   adding button for {} via AccessoryOpsModeProgrammerFacade", ProgrammingMode.OPSACCEXTBYTEMODE);
268                modes.add(ProgrammingMode.OPSACCEXTBYTEMODE);
269            }
270        }
271        log.debug("   has {} modes", modes.size());
272        for (ProgrammingMode mode : modes) {
273            JRadioButton button;
274            // need a new button?
275            if (index >= buttonPool.size()) {
276                log.debug("   add button");
277                button = new JRadioButton();
278                buttonPool.add(button);
279                modeGroup.add(button);
280                button.addActionListener(this);
281                add(button); // add to GUI
282            }
283            // configure next button in pool
284            log.debug("   set for {}", mode.toString());
285            button = buttonPool.get(index++);
286            button.setVisible(true);
287            modeGroup.add(button);
288            button.setText(mode.toString());
289            buttonMap.put(mode, button);
290        }
291
292        setGuiFromProgrammer();
293    }
294
295    /**
296     * Listen to buttons for mode changes.
297     *
298     * @param e ActionEvent heard
299     */
300    @Override
301    public void actionPerformed(java.awt.event.ActionEvent e) {
302        // find selected button
303        log.debug("Selected button: {}", e.getActionCommand());
304        for (ProgrammingMode mode : buttonMap.keySet()) {
305            if (mode.toString().equals(e.getActionCommand())) {
306                log.debug("      setting mode {} on {}", mode, getProgrammer());
307                if (getProgrammer() != null) {
308                    log.debug("getProgrammer() != null");
309                    if (mode == ProgrammingMode.OPSACCBYTEMODE) {
310                        log.debug("OPS ACCY was selected in actionPerformed");
311                        opsAccyMode = true;
312                        opsSigMode = false;
313                        lnAttachedBoardMode = false;
314                        lnSv2Mode = false ;
315                        lncvMode = false ;
316                    } else if (mode == ProgrammingMode.OPSACCEXTBYTEMODE) {
317                        log.debug("OPS SIG was selected in actionPerformed");
318                        opsAccyMode = false;
319                        opsSigMode = true;
320                        lnAttachedBoardMode = false;
321                        lnSv2Mode = false ;
322                        lncvMode = false ;
323                    } else {
324                        opsAccyMode = false;
325                        opsSigMode = false;
326                        lnAttachedBoardMode = (mode == LnProgrammerManager.LOCONETOPSBOARD);
327                        lnSv2Mode = (mode == LnProgrammerManager.LOCONETSV2MODE);
328                        lncvMode = (mode == LnProgrammerManager.LOCONETLNCVMODE);
329                        getProgrammer().setMode(mode);
330                    }
331                }
332                setAddrParams();
333                return; // 1st match
334            }
335        }
336    }
337
338    /**
339     * Change the programmer (mode).
340     *
341     * @param programmer The type of programmer (i.e. Byte Mode)
342     */
343    void setProgrammerFromGui(Programmer programmer) {
344        for (Map.Entry<ProgrammingMode, JRadioButton> entry : buttonMap.entrySet()) {
345            if (entry.getValue().isSelected()) {
346                if (entry.getKey() == ProgrammingMode.OPSACCBYTEMODE) {
347                    log.debug("OPS ACCY was selected in setProgrammerFromGui");
348                    opsAccyMode = true;
349                    opsSigMode = false;
350                    lnAttachedBoardMode = false;
351                    lnSv2Mode = false;
352                    lncvMode = false;
353                } else if (entry.getKey() == ProgrammingMode.OPSACCEXTBYTEMODE) {
354                    log.debug("OPS SIG was selected in setProgrammerFromGui");
355                    opsAccyMode = false;
356                    opsSigMode = true;
357                    lnAttachedBoardMode = false;
358                    lnSv2Mode = false;
359                    lncvMode = false;
360                } else {
361                    opsAccyMode = false;
362                    opsSigMode = false;
363                    lnAttachedBoardMode = (entry.getKey() == LnProgrammerManager.LOCONETOPSBOARD);
364                    lnSv2Mode = (entry.getKey() == LnProgrammerManager.LOCONETSV2MODE);
365                    lncvMode = (entry.getKey() == LnProgrammerManager.LOCONETLNCVMODE);
366                    getProgrammer().setMode(entry.getKey());
367                }
368            }
369        }
370    }
371
372    /**
373     * Listen to programmer for mode changes.
374     *
375     * @param e ActionEvent heard
376     */
377    @Override
378    public void propertyChange(java.beans.PropertyChangeEvent e
379    ) {
380        if ("Mode".equals(e.getPropertyName()) && getProgrammer().equals(e.getSource())) {
381            // mode changed in programmer, change GUI here if needed
382            if (isSelected()) {  // only change mode if we have a selected mode, in case some other selector with shared group has the selection
383                setGuiFromProgrammer();
384            }
385        }
386    }
387
388    /**
389     * Change the selected mode in GUI when programmer is changed elsewhere.
390     */
391    void setGuiFromProgrammer() {
392        if (getProgrammer() == null) {
393            // no mode selected
394            for (JRadioButton button : buttonPool) {
395                button.setSelected(false);
396            }
397            return;
398        }
399
400        ProgrammingMode mode = getProgrammer().getMode();
401        if (opsAccyMode) {
402            mode = ProgrammingMode.OPSACCBYTEMODE;
403        } else if (opsSigMode) {
404            mode = ProgrammingMode.OPSACCEXTBYTEMODE;
405        }
406        JRadioButton button = buttonMap.get(mode);
407        if (button == null) {
408            log.error("setGuiFromProgrammer found mode \"{}\" that's not supported by the programmer", mode);
409            return;
410        }
411        log.debug("  setting button for mode {}", mode);
412        button.setSelected(true);
413        setAddrParams();
414    }
415
416    /**
417     * Set address limits and field names depending on address type.
418     */
419    void setAddrParams() {
420        if (opsAccyMode) {
421            shortAddrButton.setText(Bundle.getMessage("DecoderAddress"));
422            shortAddrButton.setToolTipText(Bundle.getMessage("ToolTipDecoderAddress"));
423            shortAddrButton.setVisible(true);
424            longAddrButton.setText(Bundle.getMessage("AccessoryAddress"));
425            longAddrButton.setToolTipText(Bundle.getMessage("ToolTipAccessoryAddress"));
426            offsetAddrCheckBox.setVisible(false);
427            addressLabel.setText(Bundle.getMessage("AddressLabel"));
428            if (longAddrButton.isSelected()) {
429                lowAddrLimit = 1;
430                highAddrLimit = 2044;
431            } else {
432                lowAddrLimit = 1;
433                highAddrLimit = 511;
434            }
435        } else if (opsSigMode) {
436            shortAddrButton.setVisible(false);
437            longAddrButton.setVisible(false);
438            offsetAddrCheckBox.setVisible(true);
439            addressLabel.setText(Bundle.getMessage("SignalAddressLabel"));
440            lowAddrLimit = 1;
441            highAddrLimit = 2044;
442        } else if (lnAttachedBoardMode) {
443            shortAddrButton.setVisible(false);
444            longAddrButton.setVisible(false);
445            offsetAddrCheckBox.setVisible(false);
446            addressLabel.setText(Bundle.getMessage("NodeLabel"));
447            lowAddrLimit = 0;
448            highAddrLimit = 16383;
449        } else if (lnSv2Mode) {
450            shortAddrButton.setVisible(false);
451            longAddrButton.setVisible(false);
452            offsetAddrCheckBox.setVisible(false);
453            addressLabel.setText(Bundle.getMessage("NodeLabel"));
454            lowAddrLimit = 0;
455            highAddrLimit = 65535;
456        } else if (lncvMode) {
457            shortAddrButton.setVisible(false);
458            longAddrButton.setVisible(false);
459            offsetAddrCheckBox.setVisible(false);
460            addressLabel.setText(Bundle.getMessage("ModuleLabel"));
461            lowAddrLimit = 0;
462            highAddrLimit = 65535;
463        } else {
464            shortAddrButton.setText(Bundle.getMessage("ShortAddress"));
465            shortAddrButton.setToolTipText(Bundle.getMessage("ToolTipShortAddress"));
466            shortAddrButton.setVisible(true);
467            longAddrButton.setText(Bundle.getMessage("LongAddress"));
468            longAddrButton.setToolTipText(Bundle.getMessage("ToolTipLongAddress"));
469            offsetAddrCheckBox.setVisible(false);
470            addressLabel.setText(Bundle.getMessage("AddressLabel"));
471            if (longAddrButton.isSelected()) {
472                lowAddrLimit = 0;
473                highAddrLimit = 10239;
474            } else {
475                lowAddrLimit = 1;
476                highAddrLimit = 127;
477            }
478        }
479
480        log.debug("Setting lowAddrLimit={}, highAddrLimit={}", lowAddrLimit, highAddrLimit);
481        model.setMinimum(lowAddrLimit);
482
483        model.setMaximum(highAddrLimit);
484        int address;
485
486        try {
487            address = (Integer) mAddrField.getValue();
488        } catch (java.lang.NumberFormatException e) {
489            log.debug("loco address \"{}\" not correct", mAddrField.getValue());
490            return;
491        }
492        if (address < lowAddrLimit) {
493            mAddrField.setValue(lowAddrLimit);
494        } else if (address > highAddrLimit) {
495            mAddrField.setValue(highAddrLimit);
496        }
497    }
498
499// Free up memory from no longer needed stuff, disconnect if still connected.
500    @Override
501    public void dispose() {
502    }
503
504    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProgOpsModePane.class.getName());
505
506}