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}