001package jmri.jmrix.can.cbus.swing.modules.merg;
002
003import java.awt.*;
004import java.awt.event.ActionEvent;
005
006import javax.swing.*;
007import javax.swing.border.*;
008import javax.swing.event.*;
009
010import jmri.jmrix.can.cbus.node.CbusNode;
011import jmri.jmrix.can.cbus.node.CbusNodeNVTableDataModel;
012import jmri.jmrix.can.cbus.swing.modules.*;
013
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * Node Variable edit frame for a MERG CANACC8 CBUS module
019 *
020 * @author Andrew Crosland Copyright (C) 2021
021 */
022public class Canacc8EditNVPane extends AbstractEditNVPane {
023    
024    // Number of outputs
025    public static final int OUTPUTS = 8;
026    
027    // Output type
028    public static final int TYPE_CONTINUOUS = 0;
029    public static final int TYPE_SINGLE = 1;
030    public static final int TYPE_REPEAT = 2;
031    
032    // Startup action
033    public static final int ACTION_OFF = 3;
034    public static final int ACTION_SAVED = 1;
035    public static final int ACTION_NONE = 0;
036    
037    // Conversion between NV and display values
038    public static final int PULSE_WIDTH_STEP_SIZE = 20;
039    public static final int PULSE_WIDTH_NUM_STEPS = 127;
040    public static final double FEEDBACK_DELAY_STEP_SIZE = 0.5;
041    
042    OutPane [] out = new OutPane[OUTPUTS+1];
043
044    private final UpdateNV pulseUpdateFn = new UpdatePulse();
045    private final UpdateNV startupUpdateFn = new UpdateStartup();
046    private final UpdateNV feedbackUpdateFn = new UpdateFeedback();
047
048    private TitledSpinner feedbackSpinner;
049    
050    protected Canacc8EditNVPane(CbusNodeNVTableDataModel dataModel, CbusNode node) {
051        super(dataModel, node);
052    }
053    
054    /** {@inheritDoc} */
055    @Override
056    public AbstractEditNVPane getContent() {
057       
058        JPanel gridPane = new JPanel(new GridBagLayout());
059        GridBagConstraints c = new GridBagConstraints();
060        c.fill = GridBagConstraints.HORIZONTAL;
061        c.weightx = 1;
062        c.weighty = 1;
063        c.gridy = 0;
064        
065        // Four columns for the outputs
066        for (int y = 0; y < OUTPUTS/4; y++) {
067            c.gridx = 0;
068            for (int x = 0; x < 4; x++) {
069                int index = y*4 + x + 1;            // NVs indexed from 1
070                out[index] = new OutPane(index);
071                gridPane.add(out[index], c);
072                c.gridx++;
073            }
074            c.gridy++;
075        }
076
077        c.gridx = 0;
078        c.gridy = 3;
079        feedbackSpinner = new TitledSpinner(Bundle.getMessage("FeedbackDelayUnits"), Canacc8PaneProvider.FEEDBACK_DELAY, feedbackUpdateFn);
080        feedbackSpinner.setToolTip(Bundle.getMessage("FeedbackDelayTt"));
081        feedbackSpinner.init(getSelectValue8(Canacc8PaneProvider.FEEDBACK_DELAY)*FEEDBACK_DELAY_STEP_SIZE, 0, 
082                FEEDBACK_DELAY_STEP_SIZE*255, FEEDBACK_DELAY_STEP_SIZE);
083        
084        gridPane.add(feedbackSpinner, c);
085
086        JScrollPane scroll = new JScrollPane(gridPane);
087        add(scroll);
088        
089        return this;
090    }
091    
092    /** {@inheritDoc} */
093    @Override
094    public void tableChanged(TableModelEvent e) {
095        if (e.getType() == TableModelEvent.UPDATE) {
096            int row = e.getFirstRow();
097            int nv = row + 1;
098            int value = getSelectValue8(nv);
099            if ((nv > 0) && (nv <= 8)) {
100                //log.debug("Update NV {} to {}", nv, value);
101                int oldSpinnerValue = out[nv].pulseSpinner.getIntegerValue()/PULSE_WIDTH_STEP_SIZE;
102                out[nv].setButtons(value, oldSpinnerValue);
103                out[nv].pulseSpinner.setValue((value & 0x7f)*PULSE_WIDTH_STEP_SIZE);
104                log.debug("NV {} Now {}", nv, (out[nv].pulseSpinner.getIntegerValue()));
105            } else if (nv == 9) {
106                //log.debug("Update feedback delay to {}", value);
107                feedbackSpinner.setValue(value*FEEDBACK_DELAY_STEP_SIZE);
108            } else if ((nv == 10) || (nv == 11)) {
109                //log.debug("Update startup action", value);
110                for (int i = 1; i <= 8; i++) {
111                    out[i].action.setButtons();
112                }
113            } else {
114                // Not used, or row was -1
115//                log.debug("Update unknown NV {}", nv);
116            }
117        }
118    }
119    
120    /**
121     * Update the NVs controlling the pulse width and type
122     */
123    protected class UpdatePulse implements UpdateNV {
124        
125        /** {@inheritDoc} */
126        @Override
127        public void setNewVal(int index) {
128            int pulseWidth = out[index].pulseSpinner.getIntegerValue();
129            pulseWidth /= PULSE_WIDTH_STEP_SIZE;
130            if (out[index].cont.isSelected()) {
131                pulseWidth = 0;
132            }          
133            if (out[index].repeat.isSelected()) {
134                pulseWidth |= 0x80;
135            }
136            // Preserve continuous (bit 7) from old value unless we selected single button
137            if ((getSelectValue8(index) >= 0x80) && !(out[index].buttonFlag && out[index].single.isSelected())) {
138                pulseWidth |= 0x80;
139            }
140            // Note that changing the data model will result in tableChanged() being called, which can manipulate the buttons, etc
141            _dataModel.setValueAt(pulseWidth, index - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN);
142        }
143    }
144    
145    /**
146     * Update the NVs controlling the startup action
147     */
148    protected class UpdateStartup implements UpdateNV {
149        
150        @Override
151        public void setNewVal(int index) {
152            int newNV10 = getSelectValue8(Canacc8PaneProvider.STARTUP_POSITION) & (~(1<<(index-1)));
153            int newNV11 = getSelectValue8(Canacc8PaneProvider.STARTUP_MOVE) & (~(1<<(index-1)));
154            
155            // Startup action is in NV10 and NV11, 1 bit per output 
156            if (out[index].action.off.isSelected()) {
157                // 11
158                newNV10 |= (1<<(index-1));
159                newNV11 |= (1<<(index-1));
160            } else if (out[index].action.saved.isSelected()) {
161                // 01
162                newNV11 |= (1<<(index-1));
163            }
164            
165            // Note that changing the data model will result in tableChanged() being called, which can manipulate the buttons, etc
166            _dataModel.setValueAt(newNV10, Canacc8PaneProvider.STARTUP_POSITION-1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN);
167            _dataModel.setValueAt(newNV11, Canacc8PaneProvider.STARTUP_MOVE-1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN);
168        }
169    }
170    
171    /**
172     * Update the NV controlling the feedback delay
173     */
174    protected class UpdateFeedback implements UpdateNV {
175
176        /** {@inheritDoc} */
177        @Override
178        public void setNewVal(int index) {
179            double delay = feedbackSpinner.getDoubleValue();
180            int newInt = (int)(delay/FEEDBACK_DELAY_STEP_SIZE);
181            // Note that changing the data model will result in tableChanged() being called, which can manipulate the buttons, etc
182            _dataModel.setValueAt(newInt, index - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN);
183        }
184    }
185    
186    /**
187     * Construct pane to allow configuration of the module outputs
188     */
189    private class OutPane extends JPanel {
190        
191        int _index;
192        
193        protected JRadioButton cont;
194        protected JRadioButton single;
195        protected JRadioButton repeat;
196        protected TitledSpinner pulseSpinner;
197        protected StartupActionPane action;
198        protected boolean buttonFlag = false;
199
200        public OutPane(int index) {
201            super();
202            _index = index;
203            JPanel gridPane = new JPanel(new GridBagLayout());
204            GridBagConstraints c = new GridBagConstraints();
205            c.fill = GridBagConstraints.HORIZONTAL;
206            c.weightx = 1;
207            c.weighty = 1;
208            c.gridx = 0;
209            c.gridy = 0;
210
211            Border border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED);
212            TitledBorder title = BorderFactory.createTitledBorder(border, Bundle.getMessage("OutputX", _index));
213            setBorder(title);
214
215            cont = new JRadioButton(Bundle.getMessage("Continuous"));
216            cont.setToolTipText(Bundle.getMessage("ContinuousTt"));
217            single = new JRadioButton(Bundle.getMessage("Single"));
218            single.setToolTipText(Bundle.getMessage("SingleTt"));
219            repeat = new JRadioButton(Bundle.getMessage("Repeat"));
220            repeat.setToolTipText(Bundle.getMessage("RepeatTt"));
221
222            cont.addActionListener((ActionEvent e) -> {
223                typeActionListener();
224            });
225            single.addActionListener((ActionEvent e) -> {
226                typeActionListener();
227            });
228            repeat.addActionListener((ActionEvent e) -> {
229                typeActionListener();
230            });
231            
232            ButtonGroup buttons = new ButtonGroup();
233            buttons.add(cont);
234            buttons.add(single);
235            buttons.add(repeat);
236
237            pulseSpinner = new TitledSpinner(Bundle.getMessage("PulseWidth"), _index, pulseUpdateFn);
238            pulseSpinner.setToolTip(Bundle.getMessage("PulseWidthTt"));
239            pulseSpinner.init(((getSelectValue8(_index) & 0x7f)*PULSE_WIDTH_STEP_SIZE), 0, 
240                    PULSE_WIDTH_NUM_STEPS*PULSE_WIDTH_STEP_SIZE, PULSE_WIDTH_STEP_SIZE);
241
242            setButtonsInit(getSelectValue8(index));
243
244            gridPane.add(cont, c);
245            c.gridy++;
246            gridPane.add(single, c);
247            c.gridy++;
248            gridPane.add(repeat, c);
249            c.gridy++;
250            gridPane.add(pulseSpinner, c);
251            
252            c.gridx = 1;
253            c.gridy = 0;
254            c.gridheight = 4;
255            action = new StartupActionPane(_index);
256            gridPane.add(action, c);
257            
258            add(gridPane);
259        }
260        
261        /**
262         * Set Initial pulse type button states to reflect pulse width from initial NV value
263         * 
264         * @param pulseWidth 
265         */
266        protected void setButtonsInit(int pulseWidth) {
267            if ((pulseWidth == 0) || (pulseWidth == 128)) {
268                cont.setSelected(true);
269                pulseSpinner.setEnabled(false);
270            } else if (pulseWidth > 128) {
271                repeat.setSelected(true);
272            } else {
273                single.setSelected(true);
274            }                    
275        }
276        
277        /**
278         * Set pulse type button states to reflect new setting from change in
279         * table model (which may result from changes in this gui).
280         * 
281         * Changes to table data model from this gui fire a data changed event 
282         * back to us so we have a conflict between who is changing the raw 
283         * value or who is changing button states, hence the slightly complex
284         * logic.
285         * 
286         * @param pulseWidth from the table change event
287         * @param oldPulseWidth from the spinner in this edit gui
288         */
289        protected void setButtons(int pulseWidth, int oldPulseWidth) {
290            if (buttonFlag == true) {
291                // User clicked a button
292                if (cont.isSelected()) {
293                    pulseSpinner.setEnabled(false);
294                } else {
295                    pulseSpinner.setEnabled(true);
296                }
297                buttonFlag = false;
298            } else {
299                // Change came from spinner or generic NV pane
300                if (!pulseSpinner.isEnabled()) {
301                    // Spinner disabled, change from generic NV pane
302                    if ((pulseWidth != 0) && (pulseWidth != 128)) {
303                        pulseSpinner.setEnabled(true);
304                        if (pulseWidth >= 128) {
305                            repeat.setSelected(true);
306                        } else {
307                            single.setSelected(true);
308                        }
309                    } else {
310                        cont.setSelected(true);
311                    }
312                } else {
313                    // Spinner enabled so was not continuous
314                    if (pulseWidth != oldPulseWidth) {
315                        // Change of value in generic NV pane
316                        if ((pulseWidth & 0x7F) == 0) {
317                            // Continuous
318                            cont.setSelected(true);
319                            pulseSpinner.setEnabled(false);
320                        } else {
321                            if (pulseWidth >= 128) {
322                                repeat.setSelected(true);
323                            } else {
324                                single.setSelected(true);
325                            }
326                        }
327                    } else if ((pulseWidth & 0x7F) == 0) {
328                        // Change of spinner in this edit pane
329                        cont.setSelected(true);
330                        pulseSpinner.setEnabled(false);
331                    }
332                }
333            }
334        }
335        
336        /**
337         * Call the callback to update from radio button selection state.
338         */
339        protected void typeActionListener() {
340            buttonFlag = true;
341            pulseUpdateFn.setNewVal(_index);
342        }
343    }
344    
345    /**
346     * Construct pane to allow configuration of the output startup action
347     */
348    private class StartupActionPane extends JPanel {
349        
350        int _index;
351        
352        JRadioButton off;
353        JRadioButton none;
354        JRadioButton saved;
355    
356        public StartupActionPane(int index) {
357            super();
358            _index = index;
359            JPanel gridPane = new JPanel(new GridBagLayout());
360            GridBagConstraints c = new GridBagConstraints();
361            c.fill = GridBagConstraints.HORIZONTAL;
362            c.weightx = 1;
363            c.weighty = 1;
364            c.gridx = 0;
365            c.gridy = 0;
366
367            Border border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED);
368            TitledBorder title = BorderFactory.createTitledBorder(border, Bundle.getMessage("StartupAction"));
369            setBorder(title);
370
371            off = new JRadioButton(Bundle.getMessage("Off"));
372            off.setToolTipText(Bundle.getMessage("OffTt"));
373            none = new JRadioButton(Bundle.getMessage("None"));
374            none.setToolTipText(Bundle.getMessage("NoneTt"));
375            saved = new JRadioButton(Bundle.getMessage("SavedAction"));
376            saved.setToolTipText(Bundle.getMessage("SavedActionTt"));
377            
378            off.addActionListener((ActionEvent e) -> {
379                startupActionListener();
380            });
381            none.addActionListener((ActionEvent e) -> {
382                startupActionListener();
383            });
384            saved.addActionListener((ActionEvent e) -> {
385                startupActionListener();
386            });
387
388            ButtonGroup buttons = new ButtonGroup();
389            buttons.add(off);
390            buttons.add(none);
391            buttons.add(saved);
392            setButtons();
393            // Startup action is in NV10 and NV11, 1 bit per output 
394            if ((getSelectValue8(Canacc8PaneProvider.STARTUP_POSITION) & (1<<(_index-1)))>0) {
395                // 1x
396                off.setSelected(true);
397            } else if ((getSelectValue8(Canacc8PaneProvider.STARTUP_MOVE) & (1<<(_index-1)))>0) {
398                // 01
399                saved.setSelected(true);
400            } else {
401                // 00
402                none.setSelected(true);
403            }
404
405            gridPane.add(off, c);
406            c.gridy++;
407            gridPane.add(none, c);
408            c.gridy++;
409            gridPane.add(saved, c);
410            
411            add(gridPane);
412        }
413        
414        /**
415         * Set startup action button states
416         */
417        public void setButtons() {
418            // Startup action is in NV10 and NV11, 1 bit per output 
419            if ((getSelectValue8(Canacc8PaneProvider.STARTUP_POSITION) & (1<<(_index-1)))>0) {
420                // 1x
421                off.setSelected(true);
422            } else if ((getSelectValue8(Canacc8PaneProvider.STARTUP_MOVE) & (1<<(_index-1)))>0) {
423                // 01
424                saved.setSelected(true);
425            } else {
426                // 00
427                none.setSelected(true);
428            }
429        }
430        
431        /**
432         * Call the callback to update from radio button selection state.
433         */
434        protected void startupActionListener() {
435            startupUpdateFn.setNewVal(_index);
436        }
437    }
438    
439    private final static Logger log = LoggerFactory.getLogger(Canacc8EditNVPane.class);
440
441}