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